Compare commits

...
Author SHA1 Message Date
François-X. T. ea4e588ebe moderation_notes everywhere 2026-05-14 13:25:05 -04:00
François-X. T. a03ca9c500 Review comments 2026-05-14 12:25:53 -04:00
François-X. T. 77ae68979c cloned 2026-05-13 20:00:31 -04:00
François-X. T. 7883641dd6 .remove() -> .get().clone() 2026-05-13 19:53:41 -04:00
François-X. T. 32ff735dbc .body("") -> .finish() 2026-05-13 19:51:57 -04:00
François-X. T. 69e312c53c Remove double Option 2026-05-13 19:51:06 -04:00
François-X. T. 218979eabf Respect 1-indexing on moderation_notes.version default in DB migration 2026-05-13 18:55:37 -04:00
François-X. T. 5a57250e72 Remove CONSTRAINT CHECK on moderation_notes 2026-05-13 18:55:37 -04:00
François-X. T. 381cc2ed69 Fix tests 2026-05-13 18:55:37 -04:00
François-X. T. b760e9b1e2 Undo v2 compat changes 2026-05-13 18:55:37 -04:00
François-X. T. 4b2eee80d8 Accept missing If-Match if no existing note 2026-05-13 18:55:37 -04:00
François-X. T. eaf5c15075 Query cache 2026-05-13 18:55:37 -04:00
François-X. T. 0ec94ba821 Improve queries 2026-05-13 18:55:37 -04:00
François-X. T. 60a4a04ffa Use macros 2026-05-13 18:55:37 -04:00
François-X. T. c25e8f0957 Moderator notes 2026-05-13 18:55:37 -04:00
François-Xavier TalbotandGitHub e9eb98f97e push: true (#6096) 2026-05-14 00:54:37 +02:00
aecsocketandGitHub e5bbd9d409 Remove non-Typesense search backends, add default env vars (#6082)
* Remove non-Typesense search backends, add default env vars

* shear

* remove some default keys
2026-05-13 17:15:37 +00:00
Prospector 83dddfd512 changelog 2026-05-12 22:26:18 -07:00
0ffdabb2a3 feat: new proj moderation page (#6044)
* feat: new proj moderation page

* make requested changes

* add boolean for showing delay message

* fix server icon + shortened code

* fix server icon

* refactor admonitions

* msg correction.

* correction + change spam-notice

* Separate status info from instruction details

* Tweak timing delay msg, thread activity warning, and refer to moderation with consistent terms.

* Whoops, actually updated msgs correctly now.

* prepr + margin

* split out strings, simplify code again

* fix: a few more moderation fixes (#6048)

* fix: move tooltip to button

* fix: lock status buttons after pressing

* fix: unlisted/withheld icon on legacy badge

* prepprrr

* fix banners, add some extra dev mode stuff

* fix thread id copy padding

* tweak: adjust some of the status change messages (#6041)

* update messages & bunch of other stuff

* rename toggle

* change hover to 2.5, fix error size

* private msg overlay

---------

Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>
2026-05-12 22:23:18 -07:00
ProspectorandGitHub d87f93fdd5 fix: back to browse in app not working (#6087) 2026-05-12 22:22:27 -07:00
Emma F.andGitHub f6fa486dea chore: update DMCA registered agent (#6088)
These changes were made effective May 4th

Signed-off-by: Emma F. <60205699+triphora@users.noreply.github.com>
2026-05-13 00:57:46 +00:00
Prospector 78d978b22e changelog 2026-05-12 13:30:25 -07:00
François-Xavier TalbotandGitHub 71559d62c8 fix app build (#6086) 2026-05-12 13:30:13 -07:00
Prospector 538eda6976 changelog 2026-05-12 13:06:19 -07:00
ProspectorandGitHub 882b01c7c9 fix: app restarting after the user closes when there's a pending update (#6074)
* fix: app restarting after the user closes when there's a pending update

* add logging and fix tauri variable

* use state

* use atomicbool
2026-05-12 19:01:12 +00:00
ProspectorandGitHub a192f7857e fix: pagination margin and opening project pages in new tab (#6079)
* fix:

* adjust web to remove extra bottom margin too
2026-05-12 11:33:49 -07:00
ArthurandGitHub 3bf0f91cf0 fix(app): prevent browse controls from wrapping (#6080)
* Prevent browse filter from wrapping

* Remove unnecessary styling

* Only expand width when required

* Update web variant styling
2026-05-12 17:45:26 +00:00
ProspectorandGitHub 3083dcd932 fix: app caching invalid values before checking if they are valid (#6077)
* fix: app caching invalid values before checking if they are valid

* prepr
2026-05-12 17:40:29 +00:00
ProspectorandGitHub c8c79a6c74 fix: close button hitbox (#6078) 2026-05-12 10:23:56 -07:00
aecsocketandGitHub f5462b6dd8 Add Modrinth App to app user agent string (#6084)
* Add Modrinth App to app user agent string

* Simplify UA brand strings
2026-05-12 15:23:33 +00:00
aecsocketandGitHub 1ddbae40b7 Re-enable summary weight in search (#5871)
* re-enable summary weighting in search

* prioritize tokens/drop tokens thresh
2026-05-12 12:14:45 +00:00
Prospector a4f3c63fcc pin tanstack versions + set pnpm min age to 7 days 2026-05-12 01:24:38 -07:00
ProspectorandGitHub 02e10be4db fix: open modrinth project links in the app (#6072) 2026-05-11 19:57:39 -07:00
Truman GaoandGitHub e0056bfc40 feat: improve add dependency flow (#6075)
* fix: shadow on nav

* feat: improve add dependency flow

* feat: update suggested dependency style

* feat: update dependency rows to use version number and update styles

* feat: implement combobox select searched text on focus

* feat: add Tabs.vue

* feat: update nav tabs to use tabs

* feat: improve project search dropdown

* fix: dependency search not clearing inbound query

* fix: combobox no options open state bug

* feat: improve dependency project and version search
2026-05-12 02:46:23 +00:00
Truman GaoandGitHub 612934bf34 fix: cannot hover over project card tooltip items (#6071)
fix: cannot hover over project cards
2026-05-11 21:39:09 +00:00
Prospector 86d377b915 changelog 2026-05-11 13:16:18 -07:00
Prospector ad99ac039b update ads.txt 2026-05-11 13:15:34 -07:00
ProspectorandGitHub 6d3fdb680c fix: app loading speed (#6070) 2026-05-11 13:12:44 -07:00
ThatGravyBoatandGitHub 840b556c51 fix: neoforge mc version inferring incorrectly (#6068)
* fix: neoforge mc version infering

* fix: check if versionRange exists
2026-05-11 15:53:09 +00:00
Modrinth BotandGitHub 12e5f02e57 New translations from Crowdin (main) (#6067) 2026-05-11 15:43:35 +00:00
aecsocketandGitHub ca1b36efde Analytics request loader and game version validation (#6064)
* Analytics request loader and game version validation

* tweak agents

* factor tags into its own util

* lock cache refresh to avoid cache stampede

* Make analytics fields opptional
2026-05-11 14:45:50 +00:00
aecsocketandGitHub a5417e0851 Fix new analytics backend bucketing and revenue (#6052)
* Fix analytics backend QA items

* cargo prepare
2026-05-10 10:57:24 +00:00
Prospector 45398c546c changelog 2026-05-09 14:43:00 -07:00
7e769c720b Hide dotfiles from instance content scanning (#5999)
* Hide dotfiles from instance content scanning

Prevent hidden files such as .DS_Store from being treated as valid instance content.

This updates the profile scanning logic in [packages/app-lib/src/state/profiles.rs](/Users/froggy/Downloads/code-main/packages/app-lib/src/state/profiles.rs#L420) to ignore basenames that start with '.', and applies that filter consistently in both scan paths.

Signed-off-by: Corsican Frog <49497194+acorsicanfrog@users.noreply.github.com>

* Whitelist scannable instance content files

Only scan supported content archives into instance content.

Accept .jar files for mods and .zip files for datapacks, resourcepacks, and shaderpacks, after trimming the .disabled suffix. 

This prevents .DS_Store and other unsupported files from appearing in the Content tab.

Signed-off-by: Corsican Frog <49497194+acorsicanfrog@users.noreply.github.com>

* Fmt

---------

Signed-off-by: Corsican Frog <49497194+acorsicanfrog@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Co-authored-by: François-X. T. <fetch@ferrous.ch>
2026-05-09 21:38:23 +00:00
ProspectorandGitHub c1c86e3b72 fix: advanced rendering toggle in app barely worked (#6056) 2026-05-09 14:12:52 -07:00
Calum H.andGitHub c7602602e5 fix: use localstorage for sync state during install (#6057)
* fix: use localstorage for sync state during install

* fix: lint
2026-05-09 21:02:42 +00:00
Calum H.andGitHub 07f9e3aedc feat: changelog 0.13.3 (#6055) 2026-05-09 19:13:37 +00:00
a79b8e0777 feat: clean up browse shared layout logic + introduce queuing (#6030)
* feat: clean up edge case behaviour and add queued to install logic

* fix: remove version choice modal

* feat: queued flow

* feat: standardize headers in app on proj pages

* fix: clear btn

* feat: installing floating popup

* fix: lint

* fix: onboarding/reset logic change for modpacks

* qa: big ol qa

* fix: lint

* fix: lint

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-09 19:01:23 +00:00
aecsocketandGitHub 671f6d264a changelog (#6054) 2026-05-09 18:34:26 +00:00
aecsocketandGitHub e231df1f97 WebView window event handling fixes (#6038)
* WebView fixes

* UA override logic

* fix

* debug logs

* alter all webviews

* cookies stuff

* remove debug stuff
2026-05-09 15:10:51 +00:00
JerozgenandGitHub 3052a14d95 feat: make byte size units translatable (#5969)
Make byte size units translatable
2026-05-09 09:26:59 +00:00
e8665f43ca Filipino compact number plural rule (#5516)
Use `one` for compact numbers in Filipino

Signed-off-by: Jerozgen <jerozgen@gmail.com>
Co-authored-by: Calum H. <calum@modrinth.com>
2026-05-09 09:20:05 +00:00
Truman GaoandGitHub cba4550be4 feat: table component updates (#6042)
* feat: implement table header slot, empty state, and virtualization

* refactor: pnpm prepr
2026-05-08 21:29:06 +00:00
ProspectorandGitHub 384556a810 fix: instance redirects to root page with / making it appear as a sub-page in navigation (#6040) 2026-05-08 19:54:25 +00:00
Calum H.andGitHub a082e8597c fix: app user agent for api-client reqs using tauri http plugin (#6045)
fix: app user agent
2026-05-08 19:52:52 +00:00
277 changed files with 14645 additions and 6505 deletions
+1 -1
View File
@@ -164,7 +164,7 @@ jobs:
with:
context: ./apps/labrinth/docker-stage
file: ./apps/labrinth/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
annotations: ${{ steps.docker-meta.outputs.annotations }}
Generated
-24
View File
@@ -3059,29 +3059,6 @@ 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"
@@ -5275,7 +5252,6 @@ dependencies = [
"dotenv-build",
"dotenvy",
"either",
"elasticsearch",
"eyre",
"futures",
"futures-util",
-1
View File
@@ -77,7 +77,6 @@ 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",
+55 -4
View File
@@ -2,6 +2,7 @@
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
import {
AuthFeature,
ModrinthApiError,
NodeAuthFeature,
nodeAuthState,
PanelVersionFeature,
@@ -51,9 +52,10 @@ import {
providePageContext,
providePopupNotificationManager,
useDebugLogger,
useFormatBytes,
useVIntl,
} from '@modrinth/ui'
import { formatBytes, renderString } from '@modrinth/utils'
import { 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'
@@ -97,6 +99,7 @@ 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 {
@@ -106,6 +109,7 @@ import {
getUpdateSize,
isDev,
isNetworkMetered,
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import i18n from '@/i18n.config'
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
@@ -146,8 +150,9 @@ const popupNotificationManager = new AppPopupNotificationManager()
providePopupNotificationManager(popupNotificationManager)
const { addPopupNotification } = popupNotificationManager
const appVersion = getVersion()
const tauriApiClient = new TauriModrinthClient({
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
userAgent: async () => `modrinth/theseus/${await appVersion} (support@modrinth.com)`,
labrinthBaseUrl: config.labrinthBaseUrl,
archonBaseUrl: config.archonBaseUrl,
features: [
@@ -261,6 +266,8 @@ onUnmounted(async () => {
})
const { formatMessage } = useVIntl()
const formatBytes = useFormatBytes()
const messages = defineMessages({
updateInstalledToastTitle: {
id: 'app.update.complete-toast.title',
@@ -1019,11 +1026,40 @@ 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) {
@@ -1036,7 +1072,12 @@ function handleClick(e) {
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
) {
openUrl(target.href)
const parsed = parseModrinthLink(target.href)
if (target.target !== '_blank' && parsed) {
void openModrinthProjectLinkInApp(parsed)
} else {
openUrl(target.href)
}
}
e.preventDefault()
break
@@ -1567,11 +1608,15 @@ 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] {
@@ -1639,6 +1684,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
&.app-contents::before {
box-shadow: none;
}
*,
:deep(*) {
box-shadow: none !important;
--tw-drop-shadow:;
}
}
.app-sidebar::before {
@@ -1661,7 +1712,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
}
.app-contents::before {
z-index: 1;
z-index: 30;
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':
@@ -37,6 +37,6 @@ defineProps({
.progress-bar__fill {
height: 100%;
transition: width 0.3s;
transition: width 0.3s ease-out;
}
</style>
@@ -126,7 +126,7 @@ watch(
function fakeLoadingIncrease() {
if (loadingProgress.value < 95) {
setTimeout(() => {
loadingProgress.value += 1
loadingProgress.value += 2
fakeLoadingIncrease()
}, 5)
}
@@ -22,7 +22,7 @@
hover-color-fill="background"
circular
>
<button class="relative expanded-button" @click="handleClose">
<button class="relative expanded-button close-button" @click="handleClose">
<XIcon />
</button>
</ButtonStyled>
@@ -82,8 +82,12 @@ const handleClose = async () => {
</script>
<style scoped>
.expanded-button::before {
inset: -6px;
inset: -9px -6px;
content: '';
position: absolute;
}
.expanded-button.close-button::before {
inset: -9px -9px -9px -6px;
}
</style>
@@ -0,0 +1,332 @@
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}`
}
@@ -0,0 +1,83 @@
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,6 +22,10 @@ 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')
+6 -12
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,9 +29,6 @@
"app.browse.discover-servers": {
"message": "استكشف خوادم"
},
"app.browse.install-content-to-instance": {
"message": "تثبيت محتوى لنموذجك"
},
"app.browse.server.installing": {
"message": "جاري التثبيت"
},
@@ -50,9 +47,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "إسم حزمة التعديل"
},
"app.export-modal.select-files-label": {
"message": "حدد الملفات والمجلدات المراد تضمينها في الحزمة"
},
"app.export-modal.version-number-label": {
"message": "رقم الإصدار"
},
@@ -5,12 +5,6 @@
"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"
},
@@ -29,9 +23,6 @@
"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..."
},
@@ -47,9 +38,6 @@
"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"
},
+84 -3
View File
@@ -1,4 +1,88 @@
{
"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."
},
@@ -35,9 +119,6 @@
"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"
},
+21 -15
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Authentifizierungsserver sind nicht erreichbar"
},
"app.browse.add-server-to-instance": {
"app.browse.add-servers-to-instance": {
"message": "Server zu Instanz hinzufügen"
},
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zu Instanz hinzufügen"
@@ -122,6 +122,9 @@
"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"
},
@@ -131,20 +134,11 @@
"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-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.installing": {
"message": "Wird installiert"
@@ -164,6 +158,9 @@
"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"
},
@@ -171,7 +168,7 @@
"message": "Modpaketname"
},
"app.export-modal.select-files-label": {
"message": "Wähle Dateien und Ordner zum hinzufügen im Paket aus"
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -302,6 +299,15 @@
"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 Instanzen laufen"
"message": "Keine aktiven Instanzen"
},
"app.action-bar.offline": {
"message": "Offline"
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Authentifizierungsserver sind nicht erreichbar"
},
"app.browse.add-server-to-instance": {
"message": "Server zur Instanz hinzufügen"
},
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
"message": "Server zu Instanz hinzufügen"
},
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zur Instanz hinzufügen"
@@ -122,6 +122,9 @@
"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"
},
@@ -131,21 +134,9 @@
"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"
},
@@ -164,6 +155,9 @@
"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"
},
@@ -171,7 +165,7 @@
"message": "Modpackname"
},
"app.export-modal.select-files-label": {
"message": "Wähle Dateien und Ordner aus, die in das Paket sollen"
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -302,6 +296,12 @@
"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."
},
+18 -15
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Cannot reach authentication servers"
},
"app.browse.add-server-to-instance": {
"message": "Add server to instance"
},
"app.browse.add-servers-to-instance": {
"message": "Add servers to your instance"
"message": "Adding server to instance"
},
"app.browse.add-to-an-instance": {
"message": "Add to an instance"
},
"app.browse.add-to-instance": {
"message": "Add to instance"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Already added"
},
"app.browse.back-to-instance": {
"message": "Back to instance"
},
"app.browse.discover-content": {
"message": "Discover content"
},
@@ -131,20 +134,11 @@
"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.install": {
"message": "Install"
},
"app.browse.server.installed": {
"message": "Installed"
"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.installing": {
"message": "Installing"
@@ -305,6 +299,15 @@
"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."
},
+50 -47
View File
@@ -12,43 +12,43 @@
"message": "Hacer instancia principal"
},
"app.action-bar.no-instances-running": {
"message": "Ninguna instancia en ejecución"
"message": "No hay instancias en ejecución"
},
"app.action-bar.offline": {
"message": "Sin conexión"
"message": "Desconectado"
},
"app.action-bar.primary-instance": {
"message": "Instancia principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mas instancias en ejecución"
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Finalizar instancia"
"message": "Detener instancia"
},
"app.action-bar.view-active-downloads": {
"message": "Mostrar descargas activas"
"message": "Ver descargas en curso"
},
"app.action-bar.view-instance": {
"message": "Mostrar instancia"
"message": "Ver instancia"
},
"app.action-bar.view-logs": {
"message": "Mostrar registros"
"message": "Ver registros"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Habilita el renderizado avanzado, como los efectos de desenfoque, que pueden causar problemas de rendimiento sin renderizado acelerado por hardware."
"message": "Activa el renderizado avanzado, como los efectos de desenfoque, que pueden afectar el rendimiento sin aceleración por hardware."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderizado avanzado"
},
"app.appearance-settings.color-theme.description": {
"message": "Seleccione su tema de color preferido para Modrinth en este dispositivo."
"message": "Selecciona el tema de color que prefieras para la Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema de color"
"message": "Color de la interfaz"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que el launcher se abre en."
"message": "Cambia la página en la que se abre el launcher."
},
"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 predeterminada"
"message": "Página de inicio predeterminada"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desactiva la etiqueta arriba de tu personaje en la página de skins."
"message": "Desactiva la etiqueta de nombre arriba de tu personaje en la página de skins."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ocultar etiqueta"
"message": "Ocultar etiqueta de nombre"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Incluye los mundos más recientes en la sección \"Vuelve a jugar tus mundos\" en la página de inicio."
"message": "Incluye los mundos recientes en la sección \"Volver a jugar\" de la página de inicio."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Vuelve a jugar tus mundos"
"message": "Volver a jugar mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiza el launcher cuando un proceso de Minecraft empieza."
"message": "Minimiza el launcher al iniciar un proceso de Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiza el launcher"
"message": "Minimizar launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa los sistemas de frame de Windows (se requiere resetear la aplicación)."
"message": "Usar el borde de ventana del sistema (requiere reiniciar 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": "Activa la posibilidad de alternar la barra lateral."
"message": "Permite mostrar u ocultar la barra lateral."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Alternar barra lateral"
"message": "Mostrar u ocultar la barra lateral"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, te advertiremos de los riesgos antes de instalarlo."
"message": "Si intentas instalar un paquete de Modrinth (.mrpack) que no está alojado en Modrinth, te explicaremos los riesgos antes de instalarlo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Adviérteme antes de instalarme modpacks desconocidos"
"message": "Advertir antes de instalar modpacks desconocidos"
},
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
@@ -104,12 +104,6 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede 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"
},
@@ -131,21 +125,9 @@
"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"
},
@@ -153,7 +135,7 @@
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Instalando el modpack..."
"message": "Instalando modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Introduce la descripción del modpack..."
@@ -164,6 +146,9 @@
"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"
},
@@ -171,7 +156,7 @@
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Selecciona archivos y carpetas para incluirlos en el pack"
"message": "Configura que archivos incluir en esta exportación"
},
"app.export-modal.version-number-label": {
"message": "Número de la versión"
@@ -270,7 +255,7 @@
"message": "Contenido requerido"
},
"app.modal.install-to-play.header": {
"message": "Instala para jugar"
"message": "Instalar para jugar"
},
"app.modal.install-to-play.install-button": {
"message": "Instalar"
@@ -294,7 +279,7 @@
"message": "Ver contenidos"
},
"app.modal.update-to-play.header": {
"message": "Actualiza para jugar"
"message": "Actualizar para jugar"
},
"app.modal.update-to-play.update-required": {
"message": "Actualización requerida"
@@ -692,6 +677,24 @@
"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"
},
@@ -720,7 +723,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 mostrar esta advertencia otra vez"
"message": "No volver a mostrar esta advertencia"
},
"unknown-pack-warning-modal.header": {
"message": "Confirmar instalación"
@@ -729,7 +732,7 @@
"message": "Instalar de todos modos"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "El malware es normalmente distribuido por archivos de modpack que son normalmente compartidas en aplicaciones como Discord."
"message": "El malware a menudo es distribuido mediante modpacks compartidos 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,12 +104,6 @@
"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"
},
@@ -131,21 +125,9 @@
"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"
},
@@ -170,9 +152,6 @@
"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,12 +5,6 @@
"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"
},
@@ -29,9 +23,6 @@
"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..."
},
@@ -47,9 +38,6 @@
"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"
},
+189 -9
View File
@@ -1,16 +1,109 @@
{
"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"
},
@@ -29,8 +122,20 @@
"app.browse.discover-servers": {
"message": "Tumuklas ng mga server"
},
"app.browse.install-content-to-instance": {
"message": "I-install ang kontento sa instansiya"
"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.export-modal.description-placeholder": {
"message": "Ilagay ang paglalarawan ng modpack..."
@@ -41,6 +146,9 @@
"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"
},
@@ -48,7 +156,7 @@
"message": "Pangalan ng modpack"
},
"app.export-modal.select-files-label": {
"message": "Pumili ng mga talaksan at folder na isasali sa pack"
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
},
"app.export-modal.version-number-label": {
"message": "Numero ng bersiyon"
@@ -101,6 +209,15 @@
"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"
},
@@ -113,6 +230,18 @@
"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}?"
},
@@ -233,9 +362,15 @@
"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"
},
@@ -335,6 +470,12 @@
"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"
},
@@ -536,6 +677,24 @@
"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"
},
@@ -559,5 +718,26 @@
},
"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"
}
}
+24 -18
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Impossible de contacter les serveurs d'authentification"
},
"app.browse.add-server-to-instance": {
"app.browse.add-servers-to-instance": {
"message": "Ajouter le serveur à l'instance"
},
"app.browse.add-servers-to-instance": {
"message": "Ajouter des serveurs à votre instance"
"app.browse.add-to-an-instance": {
"message": "Ajouter à une instance"
},
"app.browse.add-to-instance": {
"message": "Ajouter à l'instance"
@@ -122,6 +122,9 @@
"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"
},
@@ -131,20 +134,11 @@
"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.install": {
"message": "Installer"
},
"app.browse.server.installed": {
"message": "Installé"
"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.installing": {
"message": "Installation en cours"
@@ -164,6 +158,9 @@
"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"
},
@@ -171,7 +168,7 @@
"message": "Nom du modpack"
},
"app.export-modal.select-files-label": {
"message": "Sélectionnez des fichiers et des dossiers à inclure dans le pack"
"message": "Configurez quels fichiers sont inclus dans cette exportation"
},
"app.export-modal.version-number-label": {
"message": "Numéro de version"
@@ -192,7 +189,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"
@@ -231,7 +228,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é"
@@ -258,7 +255,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..."
@@ -302,6 +299,15 @@
"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,12 +5,6 @@
"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,9 +23,6 @@
"app.browse.discover-servers": {
"message": "גלה שרתים"
},
"app.browse.install-content-to-instance": {
"message": "הוספת התוכן להתקנה"
},
"app.export-modal.description-placeholder": {
"message": "הזן את תיאור חבילת המודים..."
},
@@ -47,9 +38,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "שם חבילת המודים"
},
"app.export-modal.select-files-label": {
"message": "בחר קבצים ותיקיות להכללה בחבילה הזו"
},
"app.export-modal.version-number-label": {
"message": "מספר גרסה"
},
+45 -27
View File
@@ -6,19 +6,34 @@
"message": "Letöltések"
},
"app.action-bar.hide-more-running-instances": {
"message": "További futó példányok elrejtése"
"message": "További futó profilok elrejtése"
},
"app.action-bar.make-primary-instance": {
"message": "Elsődleges példány létrehozása"
"message": "Beállítás elsődleges profilként"
},
"app.action-bar.no-instances-running": {
"message": "Nincsenek futó példányok"
"message": "Nincsenek futó profilok"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Elsődleges példány"
"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"
},
"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."
@@ -26,12 +41,6 @@
"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"
},
@@ -44,6 +53,9 @@
"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"
},
@@ -53,24 +65,18 @@
"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..."
},
@@ -86,9 +92,6 @@
"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"
},
@@ -218,6 +221,12 @@
"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."
},
@@ -243,10 +252,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 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 a 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 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 a 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!"
@@ -631,5 +640,14 @@
},
"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."
}
}
+80 -14
View File
@@ -3,25 +3,73 @@
"message": "Mengunduh Java {version}"
},
"app.action-bar.downloads": {
"message": "Mengunduh"
"message": "Unduhan"
},
"app.action-bar.hide-more-running-instances": {
"message": "Sembunyikan lebih banyak instance yang sedang berjalan"
"message": "Sembunyikan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.make-primary-instance": {
"message": "Jadikan sebagai instance utama"
"message": "Jadikan sebagai instans utama"
},
"app.action-bar.no-instances-running": {
"message": "Tidak ada instance yang sedang berjalan"
"message": "Tidak ada instans yang sedang berjalan"
},
"app.action-bar.offline": {
"message": "Offline"
"message": "Luring"
},
"app.action-bar.primary-instance": {
"message": "Instance utama"
"message": "Instans utama"
},
"app.action-bar.show-more-running-instances": {
"message": "Tampilkan lebih banyak instance yang sedang berjalan"
"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"
},
"app.auth-servers.unreachable.body": {
"message": "Server autentikasi Minecraft mungkin sedang tidak tersedia saat ini. Periksa koneksi internet Anda dan coba lagi nanti."
@@ -29,11 +77,11 @@
"app.auth-servers.unreachable.header": {
"message": "Tidak dapat terhubung ke server autentikasi"
},
"app.browse.add-server-to-instance": {
"message": "Tambah server ke instans"
},
"app.browse.add-servers-to-instance": {
"message": "Tambah server ke instans Anda"
"message": "Menambah server ke instans"
},
"app.browse.add-to-an-instance": {
"message": "Tambah ke instans"
},
"app.browse.add-to-instance": {
"message": "Tambah ke instans"
@@ -47,14 +95,29 @@
"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.install-content-to-instance": {
"message": "Pasang konten ke instans"
"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.export-modal.description-placeholder": {
"message": "Masukkan deskripsi paket mod..."
@@ -65,6 +128,9 @@
"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"
},
@@ -72,7 +138,7 @@
"message": "Nama paket mod"
},
"app.export-modal.select-files-label": {
"message": "Pilih berkas dan folder yang ingin dimasukkan ke paket"
"message": "Konfigurasikan berkas mana yang disertakan dalam pengeksporan ini"
},
"app.export-modal.version-number-label": {
"message": "Nomor versi"
+43 -37
View File
@@ -1,12 +1,12 @@
{
"app.action-bar.downloading-java": {
"message": "Scaricando Java {version}"
"message": "Java {version}"
},
"app.action-bar.downloads": {
"message": "Download"
},
"app.action-bar.hide-more-running-instances": {
"message": "Nascondi le istanze in esecuzione"
"message": "Mostra meno 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 le istanze in esecuzione"
"message": "Mostra più istanze in esecuzione"
},
"app.action-bar.stop-instance": {
"message": "Ferma l'istanza"
},
"app.action-bar.view-active-downloads": {
"message": "Vedi i download attivi"
"message": "Mostra i download attivi"
},
"app.action-bar.view-instance": {
"message": "Vedi l'istanza"
"message": "Mostra l'istanza"
},
"app.action-bar.view-logs": {
"message": "Vedi i log"
"message": "Mostra 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 nametag"
"message": "Nascondi il nome"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Aggiunge i mondi recenti nella sezione \"Avvio rapido\" della pagina Home."
"message": "Elenca 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 quanto si avvia Minecraft."
"message": "Riduci il launcher a icona quando si avvia Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Riduci il launcher"
"message": "Riduci a icona"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa la cornice di sistema (riavvio richiesto)."
"message": "Usa la cornice di sistema (riavvio necessario)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Seleziona un'opzione"
"message": "Scegli una pagina"
},
"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": "Apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, così da conoscerne i rischi."
"message": "Visti i rischi nell'installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, mostriamo un avvertimento quando tenti di fare ciò."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami prima di installare pacchetti sconosciuti"
"message": "Avvisami se installo pacchetti sconosciuti"
},
"app.auth-servers.unreachable.body": {
"message": "I server di autenticazione di Minecraft stanno riscontrando problemi. Controlla la tua connessione a Internet e riprova più tardi."
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Impossibile raggiungere i server di autenticazione"
},
"app.browse.add-server-to-instance": {
"message": "Aggiungi server all'istanza"
},
"app.browse.add-servers-to-instance": {
"message": "Aggiungi i server alla tua istanza"
"message": "Aggiungendo il server all'istanza"
},
"app.browse.add-to-an-instance": {
"message": "Aggiungi a un'istanza"
},
"app.browse.add-to-instance": {
"message": "Aggiungi all'istanza"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Già aggiunto"
},
"app.browse.back-to-instance": {
"message": "Torna all'istanza"
},
"app.browse.discover-content": {
"message": "Esplora i contenuti"
},
@@ -131,23 +134,14 @@
"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.install": {
"message": "Installa"
},
"app.browse.server.installed": {
"message": "Installato"
"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.installing": {
"message": "Installando"
"message": "Installazione"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
@@ -164,6 +158,9 @@
"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"
},
@@ -171,7 +168,7 @@
"message": "Nome del pacchetto"
},
"app.export-modal.select-files-label": {
"message": "Seleziona i file e le cartelle da includere nel pacchetto"
"message": "Seleziona i file da includere in questa esportazione"
},
"app.export-modal.version-number-label": {
"message": "Numero di versione"
@@ -302,6 +299,15 @@
"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."
},
@@ -441,7 +447,7 @@
"message": "Nessuna amicizia corrisponde a ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cerca amicizie..."
"message": "Cerca tra le amicizie..."
},
"friends.section.heading": {
"message": "{title} - {count}"
@@ -480,7 +486,7 @@
"message": "Mondo di Minecraft"
},
"instance.edit-world.reset-icon": {
"message": "Resetta icona"
"message": "Ripristina icona"
},
"instance.edit-world.title": {
"message": "Modifica mondo"
@@ -540,7 +546,7 @@
"message": "Cambia icona"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Seleziona icona"
"message": "Scegli un'icona"
},
"instance.settings.tabs.general.library-groups": {
"message": "Gruppi della libreria"
@@ -735,7 +741,7 @@
"message": "Il loader è determinato dal server"
},
"unknown-pack-warning-modal.body": {
"message": "Un file è esaminato quando è caricato su Modrinth, indipendentemente dal suo formato (.mrpack inclusi)."
"message": "Solo i file caricati su Modrinth vengono esaminati, qualunque sia il loro formato (.mrpack inclusi)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Non mostrare più questo avviso"
@@ -747,10 +753,10 @@
"message": "Installa comunque"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Molto spesso i malware sono distribuiti attraverso i pacchetti di mod, distribuiti in piattaforme come Discord."
"message": "Spesso i malware vengono nascosti nei pacchetti di mod, poi distribuiti su piattaforme come Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Non siamo riusciti a trovare questo file su Modrinth. Consigliamo di installare file solo da fonti di cui ti fidi."
"message": "Non è stato possibile trovare questo file su Modrinth. Consigliamo di installare file solo da fonti attendibili."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Tipo di file sconosciuto"
+18 -21
View File
@@ -104,12 +104,6 @@
"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": "インスタンスに追加"
},
@@ -131,21 +125,9 @@
"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": "インストール中"
},
@@ -170,9 +152,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modパック名"
},
"app.export-modal.select-files-label": {
"message": "パックに含めるファイルとフォルダーを選択"
},
"app.export-modal.version-number-label": {
"message": "バージョン番号"
},
@@ -692,6 +671,24 @@
"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": "インスタンスによる条件"
},
+22 -19
View File
@@ -104,12 +104,6 @@
"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": "인스턴스에 추가"
},
@@ -131,21 +125,9 @@
"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": "설치 중"
},
@@ -164,6 +146,9 @@
"app.export-modal.header": {
"message": "모드팩 내보내기"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\"(을)를 포함할까요?"
},
"app.export-modal.modpack-name-label": {
"message": "모드팩 이름"
},
@@ -171,7 +156,7 @@
"message": "모드팩 이름"
},
"app.export-modal.select-files-label": {
"message": "팩에 포함할 파일과 폴더 선택"
"message": "내보내기에 어느 파일이 포함될지 구성"
},
"app.export-modal.version-number-label": {
"message": "버전 구분"
@@ -692,6 +677,24 @@
"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": "인스턴스에서 관리"
},
+166 -7
View File
@@ -5,20 +5,110 @@
"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-server-to-instance": {
"message": "Tambahkan pelayan ke dalam pemasangan"
},
"app.browse.add-servers-to-instance": {
"message": "Tambahkan pelayan ke dalam pemasangan anda"
"message": "Menambah pelayan ke dalam pemasangan"
},
"app.browse.add-to-an-instance": {
"message": "Tambahkan ke dalam pemasangan"
},
"app.browse.add-to-instance": {
"message": "Tambahkan ke dalam pemasangan"
@@ -32,14 +122,32 @@
"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.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
"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.export-modal.description-placeholder": {
"message": "Masukkan keterangan pek mod..."
@@ -50,6 +158,9 @@
"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"
},
@@ -57,7 +168,7 @@
"message": "Nama pek mod"
},
"app.export-modal.select-files-label": {
"message": "Pilih fail dan folder untuk disertakan dalam pek mod"
"message": "Konfigurasikan fail yang disertakan dalam eksport ini"
},
"app.export-modal.version-number-label": {
"message": "Nombor versi"
@@ -188,6 +299,15 @@
"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."
},
@@ -578,6 +698,24 @@
"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"
},
@@ -601,5 +739,26 @@
},
"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"
}
}
+175 -7
View File
@@ -1,15 +1,114 @@
{
"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-server-to-instance": {
"message": "Voeg server aan instantie toe"
},
"app.browse.add-servers-to-instance": {
"message": "Voeg servers aan instantie toe"
"message": "Server toevoegen aan instantie"
},
"app.browse.add-to-an-instance": {
"message": "Voeg toe aan een instantie"
},
"app.browse.add-to-instance": {
"message": "Voeg aan instantie toe"
@@ -23,14 +122,32 @@
"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.install-content-to-instance": {
"message": "Installeer inhoud naar instantie"
"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.export-modal.description-placeholder": {
"message": "Voeg modpack beschrijving in..."
@@ -41,6 +158,9 @@
"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"
},
@@ -48,7 +168,7 @@
"message": "Modpack naam"
},
"app.export-modal.select-files-label": {
"message": "Selecteer bestanden en mappen om toe te voegen aan pack"
"message": "Configureer welke bestanden bij deze export zijn betrokken"
},
"app.export-modal.version-number-label": {
"message": "Versie nummer"
@@ -179,6 +299,15 @@
"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."
},
@@ -569,6 +698,24 @@
"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"
},
@@ -592,5 +739,26 @@
},
"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"
}
}
+31 -19
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-server-to-instance": {
"message": "Dodaj serwer do instancji"
},
"app.browse.add-servers-to-instance": {
"message": "Dodaj serwery do swojej instancji"
"message": "Dodawanie serwera do instancji"
},
"app.browse.add-to-an-instance": {
"message": "Dodaj do instancji"
},
"app.browse.add-to-instance": {
"message": "Dodaj do instancji"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Już dodano"
},
"app.browse.back-to-instance": {
"message": "Powrót do instancji"
},
"app.browse.discover-content": {
"message": "Odkrywaj zawartość"
},
@@ -131,21 +134,9 @@
"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"
},
@@ -170,9 +161,6 @@
"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"
},
@@ -302,6 +290,15 @@
"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."
},
@@ -692,6 +689,21 @@
"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ę"
},
+22 -16
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-server-to-instance": {
"message": "Adicionar servidor à instância"
},
"app.browse.add-servers-to-instance": {
"message": "Adicionar servidores à sua instância"
"message": "Adicionando servidor à instância"
},
"app.browse.add-to-an-instance": {
"message": "Adicionar a uma instância"
},
"app.browse.add-to-instance": {
"message": "Adicionar à instância"
@@ -122,6 +122,9 @@
"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"
},
@@ -131,20 +134,11 @@
"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.install": {
"message": "Instalar"
},
"app.browse.server.installed": {
"message": "Instalado"
"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.installing": {
"message": "Instalando"
@@ -164,6 +158,9 @@
"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"
},
@@ -171,7 +168,7 @@
"message": "Nome do pacote de mods"
},
"app.export-modal.select-files-label": {
"message": "Selecione arquivos e pastas para incluir no pacote"
"message": "Configure quais arquivos serão incluídos nessa exportação"
},
"app.export-modal.version-number-label": {
"message": "Número da versão"
@@ -302,6 +299,15 @@
"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,12 +5,6 @@
"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"
},
@@ -29,9 +23,6 @@
"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..."
},
@@ -47,9 +38,6 @@
"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"
},
+40 -16
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,6 +122,9 @@
"app.browse.already-added": {
"message": "Уже добавлено"
},
"app.browse.back-to-instance": {
"message": "Вернуться к сборке"
},
"app.browse.discover-content": {
"message": "Поиск проектов"
},
@@ -131,20 +134,11 @@
"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-instance-content-warning": {
"message": "Добавление контента может нарушить совместимость при подключении к серверу. Также весь добавленный контент будет удалён при обновлении содержимого серверной сборки."
},
"app.browse.server.installing": {
"message": "Установка"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Экспорт сборки"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Добавить «{file}» в экспорт?"
},
"app.export-modal.modpack-name-label": {
"message": "Название сборки"
},
@@ -171,7 +168,7 @@
"message": "Название сборки"
},
"app.export-modal.select-files-label": {
"message": "Выберите файлы и папки для экспорта"
"message": "Выберите файлы для экспорта"
},
"app.export-modal.version-number-label": {
"message": "Номер версии"
@@ -302,6 +299,15 @@
"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": "Режим разработчика включён."
},
@@ -692,6 +698,24 @@
"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": "Управляется сборкой"
},
@@ -717,7 +741,7 @@
"message": "Загрузчик управляется сервером"
},
"unknown-pack-warning-modal.body": {
"message": "Проверяются только файлы, загруженные на Modrinth, в том числе файлы .mrpack."
"message": "Файлы любого формата проверены на безопасность, если они загружены на Modrinth (в том числе файлы.mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Больше не предупреждать"
@@ -5,12 +5,6 @@
"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"
},
@@ -29,9 +23,6 @@
"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..."
},
@@ -47,9 +38,6 @@
"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"
},
+12 -18
View File
@@ -1,16 +1,22 @@
{
"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"
},
@@ -29,18 +35,9 @@
"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"
},
@@ -59,9 +56,6 @@
"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"
},
+291 -78
View File
@@ -1,18 +1,111 @@
{
"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}"
@@ -23,44 +116,77 @@
"app.browse.already-added": {
"message": "เพิ่มแล้ว"
},
"app.browse.install-content-to-instance": {
"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.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": "ชื่อ ม็อดแพ็ก"
"message": "ชื่อแพ็กม็อด"
},
"app.export-modal.select-files-label": {
"message": "กำหนดไฟล์ที่ต้องการจะส่งออก"
},
"app.export-modal.version-number-label": {
"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": "โปรเจกต์"
@@ -72,7 +198,10 @@
"message": "เพิ่ม {count} โปรเจกต์เรียบร้อยแล้ว"
},
"app.instance.mods.share-text": {
"message": "มาตรวจดูโปรเจกต์ต่าง ๆ ที่ฉันใช้ในมอดแพ็กนี้กัน!"
"message": "มาตรวจดูโปรเจกต์ต่าง ๆ ที่ฉันใช้ในแพ็กม็อดนี้กัน!"
},
"app.instance.mods.share-title": {
"message": "แบ่งปันเนื้อหาแพ็กม็อด"
},
"app.instance.mods.successfully-uploaded": {
"message": "อัปโหลดสำเร็จ"
@@ -81,16 +210,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": "ออฟไลน์"
@@ -99,31 +228,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": "ติดตั้งเพื่อเล่น"
@@ -131,17 +260,20 @@
"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": "ดูเนื้อหา"
@@ -159,13 +291,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 ที่ติดตั้ง"
@@ -180,22 +312,28 @@
"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 App v{version} พร้อมให้อัปเดตแล้ว โปรดใช้ตัวจัดการแพ็กเกจ ของคุณเพื่ออัปเดตฟีเจอร์ใหม่และตัวแก้ไข่าสุด!"
"message": "Modrinth v{version} พร้อมให้อัปเดตแล้ว โปรดไปที่การจัดการแพ็กเกจของคุณสำหรับการอัปเดตคุณสมบัติใหม่และการแก้ไข่าง ๆ"
},
"app.update-popup.body.metered": {
"message": "แอฟ Modrinth v{version} พร้อมแล้ว! เนื่องจากคุณเปิดโหมดจำกัดปริมาณข้อมูล เราจึงไม่ได้อัปเดตอัตโนมัติ"
"message": "Modrinth v{version} พร้อมให้บริการแล้ว! แต่เนื่องจากคุณเปิดโหมดจำกัดปริมาณการใช้ข้อมูลเครือข่าย เราจึงไม่ได้อัปเดตเนื้อหาโดยอัตโนมัติ"
},
"app.update-popup.changelog": {
"message": "บันทึกการเปลี่ยนแปลง"
},
"app.update-popup.download": {
"message": "ดาวโหลด ({size})"
},
"app.update-popup.download-complete": {
"message": "ดาวน์โหลดเสร็จสมบูรณ์"
"message": "การดาวน์โหลดเสร็จสมบูรณ์"
},
"app.update-popup.reload": {
"message": "โหลดใหม่"
},
"app.update-popup.title": {
"message": "มีการอัปเดตใหม่"
@@ -207,13 +345,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"
@@ -222,7 +360,10 @@
"message": "เลือกตัวเลือก"
},
"app.world.world-item.incompatible-version": {
"message": "เวอร์ชัน {version} ไม่รองรับ"
"message": "ไม่สามารถเข้ากับเวอร์ชัน {version} ได้"
},
"app.world.world-item.not-played-yet": {
"message": "ยังไม่เคยเล่น"
},
"app.world.world-item.offline": {
"message": "ออฟไลน์"
@@ -233,6 +374,9 @@
"friends.action.add-friend": {
"message": "เพิ่มเพื่อน"
},
"friends.action.view-friend-requests": {
"message": "คำขอเป็นเพื่อน {count} {count, plural, other {คน}}"
},
"friends.add-friend.submit": {
"message": "ส่งคำขอเป็นเพื่อน"
},
@@ -240,13 +384,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> เพื่อเห็นว่าพวกเขากำลังเล่นอะไรอยู่!"
@@ -273,13 +417,13 @@
"message": "ออฟไลน์"
},
"friends.heading.online": {
"message": "ออไลน์"
"message": "ออไลน์"
},
"friends.heading.pending": {
"message": "กําลังดำเนินการ"
},
"friends.no-friends-match": {
"message": "ไม่มีเพื่อนชื่อว่า \"{query}\""
"message": "ไม่มีรายชื่อเพื่อนที่ตรงกับ \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "ค้นหาเพื่อน"
@@ -321,7 +465,7 @@
"message": "โลก Minecraft"
},
"instance.edit-world.reset-icon": {
"message": "รีเซ็ตรูปตัวอย่าง"
"message": "รีเซ็ตรูปสัญลักษณ์"
},
"instance.edit-world.title": {
"message": "แก้ไขโลก"
@@ -348,13 +492,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": "กำลังลบ..."
@@ -366,31 +510,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": "ใส่ชื่อกลุ่ม"
@@ -399,43 +543,58 @@
"message": "ชื่อ"
},
"instance.settings.tabs.hooks": {
"message": "ตัวดักการปล่อย"
"message": "Launch Hook"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "กำหนดเอง"
"message": "กำหนด Launch Hook เอง"
},
"instance.settings.tabs.hooks.description": {
"message": "ตัวดักจับช่วยให้ผู้ใช้ระดับสูงสามารถรันคำสั่งระบบบางอย่างได้ ทั้งก่อนและหลังการเปิดเกม"
"message": "Launch Hook จะช่วยให้ผู้เล่นที่ชำนาญสามารถใช้คำสั่งต่อระบบบางอย่างได้ ทั้งก่อนและหลังเกมถูกเปิด"
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Post-exit"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "จะรันหลังจากที่ตัวเกมถูกปิด"
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "ป้อนคำสั่งหลังจบการทำงาน..."
"message": "ป้อนคำสั่ง..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "ก่อนรัน"
"message": "Pre-launch"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "จะรันก่อนที่โปรแกรมจะทำงาน"
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "ป้อนคำสั่ง..."
},
"instance.settings.tabs.hooks.title": {
"message": "ฮุกการเปิดเกม"
"message": "Game Launch Hook"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "ตัวห่อหุ้ม"
"message": "Wrapper"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "คำสั่งตัวห่อหุ้มสำหรับเปิด ไมน์คราฟต์"
"message": "คำสั่ง Wrapper สำหรับใช้ในการรันเปิด Minecraft"
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "ป้อนตัวห่อหุ้มคำสั่ง..."
"message": "ป้อนคำสั่ง Wrapper..."
},
"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": "ฮุก"
"message": "Hook"
},
"instance.settings.tabs.java.java-arguments": {
"message": "อาร์กิวเมนต์ Java"
@@ -449,6 +608,9 @@
"instance.settings.tabs.window": {
"message": "หน้าต่าง"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "การตั้งค่าการปรับแต่งหน้าต่าง"
},
"instance.settings.tabs.window.fullscreen": {
"message": "เต็มหน้าจอ"
},
@@ -474,7 +636,7 @@
"message": "ป้อนความกว้าง..."
},
"instance.worlds.a_minecraft_server": {
"message": "เซิร์ฟเวอร์ ไมน์คราฟต์"
"message": "เซิร์ฟเวอร์ Minecraft"
},
"instance.worlds.cant_connect": {
"message": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้"
@@ -486,7 +648,7 @@
"message": "ไม่ต้องแสดงในหน้าแรก"
},
"instance.worlds.game_already_open": {
"message": "อินสแตนซ์ถูกเปิดอยู่แล้ว"
"message": "โปรแกรมถูกเปิดอยู่แล้ว"
},
"instance.worlds.hardcore": {
"message": "โหมดฮาร์ดคอร์"
@@ -507,24 +669,75 @@
"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": "อินสแตนซ์เป็นผู้จัดเตรียม"
"message": "โปรแกรมเป็นผู้กำหนด"
},
"search.filter.locked.instance-game-version.title": {
"message": "เวอร์ชันเกมถูกกำหนดไว้โดยโปรแกรมแล้ว"
},
"search.filter.locked.instance-loader.title": {
"message": "ตัวรันถูกกำหนดไว้โดยโปรแกรมแล้ว"
},
"search.filter.locked.instance.sync": {
"message": "ซิงค์กับอินสแตนซ์"
"message": "เชื่อมต่อกับโปรแกรม"
},
"search.filter.locked.server": {
"message": "เซิร์ฟเวอร์เป็นผู้จัดเตรียม"
"message": "เซิร์ฟเวอร์เป็นผู้กำหนด"
},
"search.filter.locked.server-environment.title": {
"message": "มีเพียงม็อดสำหรับฝั่งเครื่องของผู้เล่นเท่านั้นที่สามารถเพิ่มลงในโปรแกรมเซิร์ฟเวอร์ดังกล่าวได้"
},
"search.filter.locked.server-game-version.title": {
"message": "เซิร์ฟเวอร์เป็นผู้จัดเตรียมเวอร์ชันเกม"
"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": "แจ้งเตือนไฟล์ไม่รู้จัก"
}
}
+18 -21
View File
@@ -104,12 +104,6 @@
"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"
},
@@ -131,21 +125,9 @@
"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"
},
@@ -170,9 +152,6 @@
"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ı"
},
@@ -692,6 +671,24 @@
"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"
},
+9 -21
View File
@@ -104,12 +104,6 @@
"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": "Додати до профілю"
},
@@ -131,21 +125,9 @@
"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": "Встановлення"
},
@@ -170,9 +152,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Назва збірки"
},
"app.export-modal.select-files-label": {
"message": "Виберіть файли й теки, щоб додати їх до збірки"
},
"app.export-modal.version-number-label": {
"message": "Номер версії"
},
@@ -692,6 +671,15 @@
"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,12 +5,6 @@
"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ơ"
},
@@ -29,9 +23,6 @@
"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..."
},
@@ -47,9 +38,6 @@
"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"
},
+27 -21
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-server-to-instance": {
"message": "将服务器添加到实例实例"
},
"app.browse.add-servers-to-instance": {
"message": "将服务器添加到你的实例"
"message": "将服务器添加到实例"
},
"app.browse.add-to-an-instance": {
"message": "添加到实例"
},
"app.browse.add-to-instance": {
"message": "添加到实例实例"
"message": "添加到实例"
},
"app.browse.add-to-instance-name": {
"message": "添加到 {instanceName}"
@@ -120,7 +120,10 @@
"message": "已添加"
},
"app.browse.already-added": {
"message": "已添加"
"message": "已添加"
},
"app.browse.back-to-instance": {
"message": "返回实例"
},
"app.browse.discover-content": {
"message": "发现内容"
@@ -131,20 +134,11 @@
"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-instance-content-warning": {
"message": "加入服务器时,添加内容可能会破坏兼容性。当更新服务器实例内容时任何添加的内容也将丢失。"
},
"app.browse.server.installing": {
"message": "安装中"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "导出整合包"
},
"app.export-modal.include-file-accessibility-label": {
"message": "包含 \"{file}\""
},
"app.export-modal.modpack-name-label": {
"message": "整合包名称"
},
@@ -171,7 +168,7 @@
"message": "整合包名称"
},
"app.export-modal.select-files-label": {
"message": "选择要包含在包中的文件和文件夹"
"message": "配置此导出中包含哪些文件"
},
"app.export-modal.version-number-label": {
"message": "版本号"
@@ -302,6 +299,15 @@
"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": "开发者模式已启用。"
},
+30 -24
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "無法連線到驗證伺服器"
},
"app.browse.add-server-to-instance": {
"message": "將伺服器新增到實例"
},
"app.browse.add-servers-to-instance": {
"message": "將伺服器新增實例"
"message": "將伺服器新增實例"
},
"app.browse.add-to-an-instance": {
"message": "新增至實例"
},
"app.browse.add-to-instance": {
"message": "新增至實例"
@@ -120,7 +120,10 @@
"message": "已新增"
},
"app.browse.already-added": {
"message": "已新增"
"message": "已新增"
},
"app.browse.back-to-instance": {
"message": "回到實例"
},
"app.browse.discover-content": {
"message": "探索內容"
@@ -131,20 +134,11 @@
"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-instance-content-warning": {
"message": "加入內容可能會導致加入伺服器時發生相容性問題。當你更新伺服器實例內容時,任何新增的內容也會遺失。"
},
"app.browse.server.installing": {
"message": "安裝中"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "匯出模組包"
},
"app.export-modal.include-file-accessibility-label": {
"message": "要包含「{file}」嗎?"
},
"app.export-modal.modpack-name-label": {
"message": "模組包名稱"
},
@@ -171,7 +168,7 @@
"message": "模組包名稱"
},
"app.export-modal.select-files-label": {
"message": "選擇要包含在模組包中的檔案與資料夾"
"message": "設定要包含在此匯出檔案中的檔案"
},
"app.export-modal.version-number-label": {
"message": "版本號碼"
@@ -198,7 +195,7 @@
"message": "建立"
},
"app.instance.modpack-already-installed.header": {
"message": "模組包已安裝"
"message": "模組包已安裝"
},
"app.instance.modpack-already-installed.instance": {
"message": "實例"
@@ -302,6 +299,15 @@
"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": "開發人員模式已啟用。"
},
@@ -693,22 +699,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": "由該實例提供"
+288 -334
View File
@@ -5,46 +5,49 @@ import {
ClipboardCopyIcon,
ExternalIcon,
GlobeIcon,
PlayIcon,
PlusIcon,
SpinnerIcon,
StopCircleIcon,
} from '@modrinth/assets'
import type { CardAction, ProjectType, Tags } from '@modrinth/ui'
import type { BrowseInstallContentType, 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, onUnmounted, ref, shallowRef, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import type { LocationQuery } from 'vue-router'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
import { process_listener } from '@/helpers/events'
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_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 type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, get_profile_worlds, getServerLatency } from '@/helpers/worlds'
import { get_profile_worlds } from '@/helpers/worlds'
import { injectContentInstall } from '@/providers/content-install'
import { injectServerInstall } from '@/providers/server-install'
import {
@@ -52,7 +55,6 @@ 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()
@@ -76,15 +78,27 @@ const {
effectiveServerWorldId,
serverContextServerData,
serverContentProjectIds,
queuedServerInstallProjectIds,
queuedServerInstallCount,
selectedServerInstallProjects,
isInstallingQueuedServerInstalls,
queuedInstallProgress,
serverBackUrl,
serverBackLabel,
serverBrowseHeading,
clearQueuedServerInstalls,
removeQueuedServerInstall,
flushQueuedServerInstalls,
discardQueuedServerInstallsAndBack,
installQueuedServerInstallsAndBack,
initServerContext,
watchServerContextChanges,
searchServerModpacks,
getServerProjectVersions,
enforceSetupModpackRoute,
installProjectToServer,
getQueuedServerInstallPlans,
setQueuedServerInstallPlans,
openServerModpackInstallFlow,
onServerFlowBack,
handleServerModpackFlowCreate,
markServerProjectInstalled,
@@ -254,6 +268,7 @@ const instanceFilters = computed(() => {
})
const serverHideInstalled = ref(false)
const hideSelectedServerInstalls = ref(false)
if (route.query.shi) {
serverHideInstalled.value = route.query.shi === 'true'
}
@@ -291,6 +306,12 @@ 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') {
@@ -313,134 +334,24 @@ const combinedProvidedFilters = computed(() =>
isServerContext.value ? serverContextFilters.value : instanceFilters.value,
)
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 {
serverPings,
contextMenuRef,
updateServerHits,
getServerModpackContent,
getServerCardActions,
handleRightClick,
handleOptionsClick,
} = useAppServerBrowse({
instance,
isFromWorlds,
allInstalledIds,
newlyInstalled,
installingServerProjects,
playServerProject,
showAddServerToInstanceModal,
handleError,
router,
})
const offline = ref(!navigator.onLine)
@@ -454,29 +365,13 @@ 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: 'Add servers to your instance',
defaultMessage: 'Adding server to 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',
addToAnInstance: {
id: 'app.browse.add-to-an-instance',
defaultMessage: 'Add to an instance',
},
discoverContent: {
id: 'app.browse.discover-content',
@@ -502,26 +397,19 @@ 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',
@@ -654,46 +542,6 @@ 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 {
@@ -707,6 +555,15 @@ 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) {
@@ -716,13 +573,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: 'Back to instance',
backLabel: formatMessage(messages.backToInstance),
heading: formatMessage(
isFromWorlds.value ? messages.addServersToInstance : messages.installContentToInstance,
isFromWorlds.value ? messages.addServersToInstance : commonMessages.installingContentLabel,
),
warning:
isServerInstance.value && !isFromWorlds.value
? 'Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content.'
? formatMessage(messages.serverInstanceContentWarning)
: undefined,
}
}
@@ -741,71 +598,82 @@ 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') {
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
return getServerCardActions(result as Labrinth.Search.v3.ResultSearchProject)
}
// Non-server project actions
@@ -817,39 +685,84 @@ function getCardActions(
const isInstalled =
projectResult.installed ||
allInstalledIds.value.has(projectResult.project_id || '') ||
serverContentProjectIds.value.has(projectResult.project_id || '')
serverContentProjectIds.value.has(projectResult.project_id || '') ||
serverContextServerData.value?.upstream?.project_id === 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(
isInstalling
? messages.installingToServer
: isInstalled
? messages.installedToServer
: messages.installToServer,
),
icon: isInstalled ? CheckIcon : PlusIcon,
iconClass: isInstalling ? 'animate-spin' : undefined,
disabled: isInstalled || isInstalling,
color: 'brand',
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',
type: 'outlined',
onClick: async () => {
setProjectInstalling(projectResult.project_id, true)
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)
}
try {
const didInstall = await installProjectToServer(projectResult)
if (didInstall !== false) {
onSearchResultInstalled(projectResult.project_id)
}
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,
}),
})
} catch (err) {
handleError(err as Error)
} finally {
setProjectInstalling(projectResult.project_id, false)
if (shouldShowInstalling) {
setProjectInstalling(projectResult.project_id, false)
}
}
},
},
@@ -862,13 +775,15 @@ function getCardActions(
return [
{
key: 'install',
label: isInstalling
? 'Installing'
: isInstalled
? 'Installed'
: shouldUseInstallIcon
? 'Install'
: 'Add to an instance',
label: formatMessage(
isInstalling
? messages.installingToServer
: isInstalled
? commonMessages.installedLabel
: shouldUseInstallIcon
? commonMessages.installButton
: messages.addToAnInstance,
),
icon: isInstalling ? SpinnerIcon : isInstalled ? CheckIcon : PlusIcon,
iconClass: isInstalling ? 'animate-spin' : undefined,
disabled: isInstalled || isInstalling,
@@ -876,28 +791,39 @@ function getCardActions(
type: 'outlined',
onClick: async () => {
setProjectInstalling(projectResult.project_id, true)
await installVersion(
projectResult.project_id,
null,
instance.value ? instance.value.path : null,
'SearchCard',
(versionId) => {
try {
const selectedInstall = instance.value
? await chooseInstanceInstallVersion(projectResult, currentProjectType)
: { versionId: null as string | null }
if (selectedInstall === null) {
setProjectInstalling(projectResult.project_id, false)
if (versionId) {
onSearchResultInstalled(projectResult.project_id)
}
},
(profile) => {
router.push(`/instance/${profile}`)
},
{
preferredLoader: instance.value?.loader ?? undefined,
preferredGameVersion: instance.value?.game_version ?? undefined,
},
).catch((err) => {
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) {
setProjectInstalling(projectResult.project_id, false)
handleError(err)
})
}
},
},
]
@@ -937,9 +863,7 @@ async function search(requestParams: string) {
if (isServer) {
const hits = rawResults.result.hits ?? []
lastServerHits.value = hits
pingServerHits(hits)
checkServerRunningStates(hits)
updateServerHits(hits)
return {
projectHits: [],
serverHits: hits,
@@ -1024,6 +948,12 @@ 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(
@@ -1036,16 +966,26 @@ 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: instance.value ? { i: instance.value.path } : undefined,
query: getProjectBrowseQuery(),
}),
getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) => ({
path: `/project/${result.slug ?? result.project_id}`,
query: getProjectBrowseQuery(),
}),
getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) =>
`/project/${result.slug ?? result.project_id}`,
selectableProjectTypes,
showProjectTypeTabs: computed(() => !isServerContext.value),
variant: 'app',
@@ -1068,8 +1008,18 @@ provideBrowseManager({
() => (isServerContext.value && projectType.value !== 'modpack') || !!instance.value,
),
hideInstalledLabel: computed(() =>
formatMessage(isFromWorlds.value ? messages.hideAddedServers : messages.hideInstalledContent),
formatMessage(
isFromWorlds.value ? messages.hideAddedServers : commonMessages.hideInstalledContentLabel,
),
),
hideSelected: hideSelectedServerInstalls,
showHideSelected: computed(
() =>
isServerContext.value &&
projectType.value !== 'modpack' &&
queuedServerInstallCount.value > 0,
),
hideSelectedLabel: computed(() => formatMessage(commonMessages.hideSelectedContentLabel)),
onInstalled: onSearchResultInstalled,
serverPings,
getServerModpackContent,
@@ -1084,8 +1034,12 @@ provideBrowseManager({
<BrowsePageLayout>
<template #after>
<ContextMenu ref="contextMenuRef" @option-clicked="handleOptionsClick">
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
<template #open_link>
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
</template>
<template #copy_link>
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
</template>
</ContextMenu>
</template>
</BrowsePageLayout>
+239 -19
View File
@@ -45,7 +45,13 @@
/>
</Teleport>
<div class="flex flex-col gap-4 p-6">
<InstanceIndicator v-if="instance" :instance="instance" />
<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" />
<template v-if="data">
<Teleport
v-if="themeStore.featureFlags.project_background"
@@ -64,7 +70,7 @@
<ButtonStyled v-if="serverPlaying" size="large" color="red">
<button @click="handleStopServer">
<StopCircleIcon />
Stop
{{ formatMessage(commonMessages.stopButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
@@ -73,11 +79,18 @@
@click="handleClickPlay"
>
<PlayIcon />
{{ data && installingServerProjects.includes(data.id) ? 'Installing...' : 'Play' }}
{{
data && installingServerProjects.includes(data.id)
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton)
}}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular>
<button v-tooltip="'Add server to instance'" @click="handleAddServerToInstance">
<button
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
@click="handleAddServerToInstance"
>
<PlusIcon />
</button>
</ButtonStyled>
@@ -111,13 +124,17 @@
<template v-else #actions>
<ButtonStyled size="large" color="brand">
<button
v-tooltip="installed ? `This project is already installed` : null"
:disabled="installed || installing"
v-tooltip="installButtonTooltip"
:disabled="installButtonDisabled"
@click="install(null)"
>
<DownloadIcon v-if="!installed && !installing" />
<CheckIcon v-else-if="installed" />
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
<SpinnerIcon
v-if="installButtonLoading && !installButtonInstalled"
class="animate-spin"
/>
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
<CheckIcon v-else />
{{ installButtonLabel }}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
@@ -166,7 +183,7 @@
:links="[
{
label: 'Description',
href: `/project/${$route.params.id}`,
href: projectDescriptionHref,
},
{
label: 'Versions',
@@ -176,7 +193,7 @@
},
{
label: 'Gallery',
href: `/project/${$route.params.id}/gallery`,
href: projectGalleryHref,
shown: data.gallery.length > 0,
},
]"
@@ -195,11 +212,39 @@
</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 /> Install </template>
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
<template #install>
<DownloadIcon /> {{ formatMessage(commonMessages.installButton) }}
</template>
<template #open_link>
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
</template>
<template #copy_link>
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
</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>
@@ -216,10 +261,16 @@ import {
PlayIcon,
PlusIcon,
ReportIcon,
SpinnerIcon,
StopCircleIcon,
} from '@modrinth/assets'
import {
BrowseInstallHeader,
ButtonStyled,
commonMessages,
CreationFlowModal,
defineMessages,
getTargetInstallPreferences,
injectNotificationManager,
NavTabs,
OverflowMenu,
@@ -231,7 +282,11 @@ 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'
@@ -249,6 +304,7 @@ 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,
@@ -260,6 +316,7 @@ 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'
@@ -272,6 +329,22 @@ 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()
@@ -296,6 +369,11 @@ 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) {
@@ -315,11 +393,9 @@ const instanceFilters = computed(() => {
return { l: loaders, g: instance.value.game_version }
})
const versionsHref = computed(() => {
const base = `/project/${route.params.id}/versions`
const filters = instanceFilters.value
function buildProjectHref(path, extraQuery = {}) {
const params = new URLSearchParams()
for (const [key, val] of Object.entries(filters)) {
for (const [key, val] of Object.entries({ ...route.query, ...extraQuery })) {
if (Array.isArray(val)) {
for (const v of val) params.append(key, v)
} else if (val) {
@@ -327,7 +403,102 @@ const versionsHref = computed(() => {
}
}
const qs = params.toString()
return qs ? `${base}?${qs}` : base
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
})
const [allLoaders, allGameVersions] = await Promise.all([
@@ -499,6 +670,55 @@ 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,9 +176,10 @@ import {
ButtonStyled,
Card,
CopyCode,
useFormatBytes,
useFormatDateTime,
} from '@modrinth/ui'
import { formatBytes, renderString } from '@modrinth/utils'
import { renderString } from '@modrinth/utils'
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
@@ -191,6 +192,7 @@ const formatDateTime = useFormatDateTime({
timeStyle: 'short',
dateStyle: 'long',
})
const formatBytes = useFormatBytes()
const breadcrumbs = useBreadcrumbs()
@@ -486,7 +486,7 @@ 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')
@@ -513,7 +513,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() {
@@ -666,7 +666,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,22 +1,35 @@
import type { Archon, Labrinth } from '@modrinth/api-client'
import type { AbstractModrinthClient, 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'>
interface ServerModpackSelectionRequest {
export interface ServerModpackSelectionRequest {
projectId: string
versionId: string
name: string
@@ -40,9 +53,19 @@ 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: (
@@ -51,7 +74,11 @@ export interface ServerInstallContentContext {
) => Promise<Labrinth.Projects.v2.SearchResult>
getServerProjectVersions: (projectId: string) => Promise<{ id: string }[]>
enforceSetupModpackRoute: (currentProjectType: string | undefined) => void
installProjectToServer: (project: InstallableSearchResult) => Promise<boolean>
getQueuedServerInstallPlans: () => Map<string, BrowseInstallPlan<InstallableSearchResult>>
setQueuedServerInstallPlans: (
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
) => void
openServerModpackInstallFlow: (request: ServerModpackSelectionRequest) => Promise<void>
onServerFlowBack: () => void
handleServerModpackFlowCreate: (config: CreationFlowContextValue) => Promise<void>
markServerProjectInstalled: (id: string) => void
@@ -65,6 +92,114 @@ 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>
}) {
@@ -90,8 +225,22 @@ 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'
@@ -110,9 +259,9 @@ export function createServerInstallContent(opts: {
})
const serverBrowseHeading = computed(() => {
if (serverFlowFrom.value === 'reset-server') {
return 'Select modpack to install after reset'
return 'Selecting modpack to install after reset'
}
return 'Install content to server'
return 'Installing content'
})
async function resolveServerContextWorldId(serverId: string) {
@@ -134,7 +283,11 @@ 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)
}
@@ -159,6 +312,7 @@ export function createServerInstallContent(opts: {
}
if (resolvedWorldId) {
queuedServerInstalls.value = readStoredServerInstallQueue(sid, resolvedWorldId)
await refreshServerInstalledContent(sid, resolvedWorldId)
}
}
@@ -168,11 +322,15 @@ 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) {
@@ -180,28 +338,16 @@ 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({
@@ -248,82 +394,135 @@ export function createServerInstallContent(opts: {
ctx.modal.value?.setStage('final-config')
}
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
}
throw new Error('This content type cannot be installed to a server from browse.')
function clearQueuedServerInstalls() {
setQueuedServerInstallPlans(new Map())
}
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.')
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
}
writeStoredServerInstallQueue(serverId, worldId, plans)
}
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')
}
async function flushQueuedServerInstalls(
serverId: string | null = serverIdQuery.value,
worldId: string | null = effectiveServerWorldId.value,
) {
if (isInstallingQueuedServerInstalls.value) return false
await openServerModpackInstallFlow({
projectId: project.project_id,
versionId,
name: project.name,
iconUrl: project.icon_url ?? undefined,
})
if (!serverId || !worldId) {
handleError(new Error('No server world is available for install.'))
return false
}
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)
const queuedPlans = getStoredServerAddonInstallQueue<InstallableSearchResult>(serverId, worldId)
if (queuedPlans.size === 0) return true
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)))
isInstallingQueuedServerInstalls.value = true
queuedInstallProgress.value = {
completed: 0,
total: queuedPlans.size,
}
let matchingVersion = versions.find(
(version) => hasGameVersionMatch(version) && hasLoaderMatch(version),
)
if (!matchingVersion) {
matchingVersion = versions.find((version) => hasLoaderMatch(version))
}
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.')
}
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),
})
await client.archon.content_v1.addAddon(sid, wid, {
project_id: matchingVersion.project_id,
version_id: matchingVersion.id,
})
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 }
}
}
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))
}
await router.push(backUrl)
void flushQueuedServerInstalls(sid, wid)
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()
}
@@ -377,15 +576,27 @@ 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,
installProjectToServer,
getQueuedServerInstallPlans,
setQueuedServerInstallPlans,
openServerModpackInstallFlow,
onServerFlowBack,
handleServerModpackFlowCreate,
markServerProjectInstalled,
+261 -19
View File
@@ -1,4 +1,6 @@
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};
@@ -14,6 +16,156 @@ 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")
@@ -30,10 +182,11 @@ 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 app = app.clone();
let refresh_app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
if let Some(webview) = app.webviews().get_mut("ads-window")
if let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(AD_LINK.parse().unwrap());
}
@@ -43,6 +196,34 @@ 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![
@@ -103,31 +284,38 @@ 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",
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),
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
}),
// set both the `hide`/`show` state and `position`,
// to ensure that the webview is actually shown/hidden
if state.shown {
@@ -140,15 +328,68 @@ 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() }
@@ -166,9 +407,9 @@ pub async fn init_ads_window<R: Runtime>(
None
};
let Some(webview) = webview.clone() else {
if webview.is_none() {
return Ok(());
};
}
// tauri::async_runtime::spawn(async move {
// loop {
@@ -249,6 +490,7 @@ 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);
}
}
+52 -14
View File
@@ -6,6 +6,7 @@
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::*;
@@ -96,6 +97,17 @@ 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() {
@@ -245,6 +257,7 @@ fn main() {
get_update_size,
enqueue_update_for_installation,
remove_enqueued_update,
set_restart_after_pending_update,
toggle_decorations,
show_window,
restart_app,
@@ -262,7 +275,13 @@ fn main() {
#[cfg(feature = "updater")]
if matches!(event, tauri::RunEvent::Exit) {
let update_data = app.state::<PendingUpdateData>().inner();
if let Some((update, data)) = &*update_data.0.lock().unwrap() {
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()
{
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?;
@@ -271,27 +290,46 @@ 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()));
if let Err(e) = update.install(data) {
tracing::error!("Error while updating: {e}");
set_changelog_toast(None);
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);
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",
+47 -61
View File
@@ -17,74 +17,60 @@
</label>
</template>
<script>
import { fileIsValid } from '~/helpers/fileUtils.js'
<script setup lang="ts">
import { useFormatBytes } from '@modrinth/ui'
import { fileIsValid } from '@modrinth/utils'
import { ref } from 'vue'
export default {
components: {},
props: {
prompt: {
type: String,
default: 'Select file',
},
multiple: {
type: Boolean,
default: false,
},
accept: {
type: String,
default: null,
},
const props = withDefaults(
defineProps<{
prompt?: string
multiple?: boolean
accept?: string
/**
* The max file size in bytes
*/
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,
},
maxSize?: number | null
showIcon?: boolean
shouldAlwaysReset?: boolean
longStyle?: boolean
disabled?: boolean
}>(),
{
prompt: 'Select file',
multiple: false,
showIcon: true,
shouldAlwaysReset: false,
longStyle: false,
disabled: false,
},
emits: ['change'],
data() {
return {
files: [],
}
},
methods: {
addFiles(files, shouldNotReset) {
if (!shouldNotReset || this.shouldAlwaysReset) {
this.files = files
}
)
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
const emit = defineEmits<{ change: [files: File[]] }>()
if (this.files.length > 0) {
this.$emit('change', this.files)
}
},
handleDrop(e) {
this.addFiles(e.dataTransfer.files)
},
handleChange(e) {
this.addFiles(e.target.files)
},
},
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)
}
</script>
@@ -1,5 +1,12 @@
<script setup lang="ts">
import { BlueskyIcon, DiscordIcon, GithubIcon, MastodonIcon, TwitterIcon } from '@modrinth/assets'
import {
BlueskyIcon,
DiscordIcon,
GithubIcon,
MastodonIcon,
ToggleRightIcon,
TwitterIcon,
} from '@modrinth/assets'
import {
AutoLink,
ButtonStyled,
@@ -10,6 +17,7 @@ import {
type MessageDescriptor,
useVIntl,
} from '@modrinth/ui'
import { commonSettingsMessages } from '@modrinth/ui/src/utils/common-messages.js'
import TextLogo from '~/components/brand/TextLogo.vue'
@@ -233,11 +241,21 @@ function developerModeIncrement() {
role="region"
:aria-label="formatMessage(messages.modrinthInformation)"
>
<TextLogo
aria-hidden="true"
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
@click="developerModeIncrement()"
/>
<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>
<div class="flex flex-wrap justify-center gap-px sm:-mx-2">
<ButtonStyled
v-for="(social, index) in socialLinks"
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
import { XCircleIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const tempIgnored = ref(false)
const messages = defineMessages({
title: {
@@ -13,16 +17,34 @@ 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="errors?.length" variant="error">
<PagewideBanner
v-if="
flags.showAllBanners || (errors?.length && !tempIgnored && !flags.alwaysIgnoreErrorBanner)
"
variant="error"
>
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
@@ -34,5 +56,19 @@ defineProps<{
})
}}
</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,6 +13,7 @@ import {
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const config = useRuntimeConfig()
const route = useRoute()
const messages = defineMessages({
title: {
@@ -21,7 +22,7 @@ const messages = defineMessages({
},
description: {
id: 'layout.banner.preview.description',
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}.`,
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}.`,
},
})
@@ -29,10 +30,12 @@ function hidePreviewBanner() {
flags.value.hidePreviewBanner = true
saveFeatureFlags()
}
const url = computed(() => `https://modrinth.com${route.fullPath}`)
</script>
<template>
<PagewideBanner v-if="!flags.hidePreviewBanner" variant="info">
<PagewideBanner v-if="!flags.hidePreviewBanner || flags.showAllBanners" variant="info">
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
@@ -45,9 +48,9 @@ function hidePreviewBanner() {
branch: config.public.branch,
}"
>
<template #link="{ children }">
<a href="https://modrinth.com" target="_blank" rel="noopener" class="text-link">
<component :is="() => normalizeChildren(children)" />
<template #url>
<a :href="url" target="_blank" rel="noopener" class="text-link">
{{ url }}
</a>
</template>
<template #branch-link="{ children }">
@@ -75,7 +78,7 @@ function hidePreviewBanner() {
</IntlFormatted>
</span>
</template>
<template #actions_right>
<template #actions_top_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>
<ButtonStyled type="transparent" hover-color-fill="background">
<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_right>
<template #actions_top_right>
<ButtonStyled circular type="transparent">
<button
v-tooltip="formatMessage(commonMessages.closeButton)"
@@ -10,6 +10,7 @@ import {
const { formatMessage } = useVIntl()
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const messages = defineMessages({
title: {
@@ -29,14 +30,14 @@ function hideStagingBanner() {
</script>
<template>
<PagewideBanner v-if="!cosmetics.hideStagingBanner" variant="warning">
<PagewideBanner v-if="flags.showAllBanners || !cosmetics.hideStagingBanner" variant="warning">
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
<template #description>
{{ formatMessage(messages.description) }}
</template>
<template #actions_right>
<template #actions_top_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>
<ButtonStyled>
<template #actions_right>
<ButtonStyled color="red">
<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>
<template #actions_right>
<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>
<template #actions_right>
<div class="flex w-fit flex-row">
<ButtonStyled color="red">
<nuxt-link to="https://support.modrinth.com" target="_blank" rel="noopener">
@@ -96,14 +96,12 @@ async function handleResendEmailVerification() {
}}
</span>
</template>
<template #actions>
<ButtonStyled v-if="hasEmail">
<button @click="handleResendEmailVerification">
<template #actions_right>
<ButtonStyled color="orange">
<button v-if="hasEmail" @click="handleResendEmailVerification">
{{ formatMessage(verifyEmailBannerMessages.action) }}
</button>
</ButtonStyled>
<ButtonStyled v-else>
<nuxt-link to="/settings/account">
<nuxt-link v-else to="/settings/account">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(addEmailBannerMessages.action) }}
</nuxt-link>
@@ -0,0 +1,41 @@
<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,28 +2,29 @@
<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="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="flex min-w-0 flex-1 items-center justify-start gap-2">
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
<span v-tooltip="name || projectId" class="truncate font-semibold text-contrast">
<span
v-tooltip="name || projectId"
class="min-w-0 max-w-fit flex-1 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>
<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">
<div v-if="!hideRemove" class="flex shrink-0 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" />
@@ -43,12 +44,12 @@ const emit = defineEmits<{
(e: 'remove'): void
}>()
const { projectId, name, icon, dependencyType, versionName, hideRemove } = defineProps<{
const { projectId, name, icon, dependencyType, versionNumber, hideRemove } = defineProps<{
projectId: string
name?: string
icon?: string
dependencyType: Labrinth.Versions.v2.DependencyType
versionName?: string
versionNumber?: string
hideRemove?: boolean
}>()
@@ -8,7 +8,7 @@
:name="dependency.name"
:icon="dependency.icon"
:dependency-type="dependency.dependencyType"
:version-name="dependency.versionName"
:version-number="dependency.versionNumber"
: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 versionName = dependencyVersions.value[dep.version_id || '']?.name ?? ''
const versionNumber = dependencyVersions.value[dep.version_id || '']?.version_number ?? ''
if (!dependencyProject && projectsFetchLoading.value) return null
@@ -44,7 +44,7 @@ const addedDependencies = computed(() =>
name: dependencyProject?.name,
icon: dependencyProject?.icon_url,
dependencyType: dep.dependency_type,
versionName,
versionNumber,
}
})
.filter(Boolean),
@@ -3,11 +3,16 @@
v-model="projectId"
placeholder="Select project"
:options="options"
:searchable="true"
:search-value="selectedProjectOption?.label"
search-placeholder="Search by name or paste ID..."
:no-options-message="searchLoading ? 'Loading...' : 'No results found'"
:disable-search-filter="true"
searchable
disable-search-filter
select-search-text-on-focus
:show-chevron="false"
@search-input="(query) => handleSearch(query)"
@search-blur="handleSearchBlur"
@select="handleSelect"
/>
</template>
@@ -15,15 +20,37 @@
import type { ComboboxOption } from '@modrinth/ui'
import { Combobox, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import { useDebounceFn } from '@vueuse/core'
import { defineAsyncComponent, h } from 'vue'
import { defineAsyncComponent, h, markRaw, ref, watch } 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()
@@ -53,36 +80,77 @@ const search = async (query: string) => {
facets: [[`project_id:${query.replace(/[^a-zA-Z0-9]/g, '')}`]], // remove any non-alphanumeric characters
})
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',
}),
}),
),
),
}))
if (query !== latestSearchQuery) return
options.value = [...resultsByProjectId.hits, ...results.hits].map(hitToOption)
} catch (error: any) {
if (query !== latestSearchQuery) return
addNotification({
title: 'An error occurred',
text: error.data ? error.data.description : error,
type: 'error',
})
}
searchLoading.value = false
if (query === latestSearchQuery) {
searchLoading.value = false
}
}
const throttledSearch = useDebounceFn(search, 500)
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 handleSearch = async (query: string) => {
searchLoading.value = true
await throttledSearch(query)
await runSearch(query, true)
}
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-name="dependency.versionName"
:version-number="dependency.versionNumber"
@on-add-suggestion="
() =>
handleAddSuggestion({
@@ -1,28 +1,31 @@
<template>
<div
class="flex items-center justify-between gap-2 rounded-xl border-2 border-dashed border-surface-5 px-4 py-1 text-button-text"
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"
>
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="flex min-w-0 flex-1 items-center justify-start gap-2">
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
<span v-tooltip="name || 'Unknown Project'" class="truncate font-semibold text-contrast">
<span
v-tooltip="name || 'Unknown Project'"
class="min-w-0 max-w-fit flex-1 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>
<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">
<div class="flex shrink-0 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" />
@@ -41,11 +44,11 @@ const emit = defineEmits<{
(e: 'onAddSuggestion'): void
}>()
const { name, icon, dependencyType, versionName } = defineProps<{
const { name, icon, dependencyType, versionNumber } = defineProps<{
name?: string
icon?: string
dependencyType: Labrinth.Versions.v2.DependencyType
versionName?: string
versionNumber?: string
}>()
function emitAddSuggestion() {
@@ -1,11 +1,10 @@
<template>
<NavTabs
<Tabs
v-if="editingVersion"
mode="local"
:links="editTabLinks"
:active-index="2"
class="mb-4 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
value="add-files"
:tabs="editTabs"
class="mb-5 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
/>
<div class="flex w-full flex-col gap-4">
<template
@@ -99,7 +98,8 @@ import {
defineMessages,
DropzoneFileInput,
injectProjectPageContext,
NavTabs,
Tabs,
type TabsTab,
useVIntl,
} from '@modrinth/ui'
import { acceptFileFromProjectType } from '@modrinth/utils'
@@ -124,18 +124,14 @@ const {
handleNewFiles,
} = injectManageVersionContext()
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
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 setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
}
function handleRemoveFile(index: number) {
@@ -1,182 +1,58 @@
<template>
<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>
<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>
<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>
<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>
<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 v-if="addedDependencies.length" class="flex flex-col gap-4">
<span class="font-semibold text-contrast">Added dependencies</span>
<DependenciesList />
</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>
</template>
<script lang="ts" setup>
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 { Combobox } from '@modrinth/ui'
import { computed } from 'vue'
import DependencySelect from '~/components/ui/create-project-version/components/DependencySelect.vue'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
import DependenciesList from '../components/DependenciesList.vue'
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
const { newDependencyProjectId, newDependencyType, newDependencyVersionId, newDependencyVersions } =
injectManageVersionContext()
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 newDependencyVersionOptions = computed(() => [
{ label: 'Any version', value: null },
...newDependencyVersions.value,
])
const selectedNewDependencyVersionLabel = computed(
() =>
newDependencyVersionOptions.value.find(
(option) => option.value === newDependencyVersionId.value,
)?.label,
)
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,11 +1,10 @@
<template>
<NavTabs
<Tabs
v-if="editingVersion"
mode="local"
:links="editTabLinks"
:active-index="1"
class="mb-4 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
value="add-details"
:tabs="editTabs"
class="mb-5 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
/>
<div class="flex w-full flex-col gap-6">
<div class="flex flex-col gap-2">
@@ -18,6 +17,7 @@
:never-empty="true"
:capitalize="true"
:disabled="isUploading"
hide-checkmark-icon
/>
</div>
<div class="flex flex-col gap-2">
@@ -61,25 +61,21 @@
</template>
<script lang="ts" setup>
import { Chips, MarkdownEditor, NavTabs, StyledInput } from '@modrinth/ui'
import { Chips, MarkdownEditor, StyledInput, Tabs, type TabsTab } 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 = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
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 setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
}
async function onImageUpload(file: File) {
@@ -1,11 +1,10 @@
<template>
<NavTabs
<Tabs
v-if="editingVersion"
mode="local"
:links="editTabLinks"
:active-index="0"
class="mb-2 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
value="metadata"
:tabs="editTabs"
class="mb-3 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
/>
<div class="flex flex-col gap-6">
<div v-if="!editingVersion" class="flex flex-col gap-1">
@@ -161,40 +160,32 @@
</template>
<template v-if="!noDependenciesProject">
<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>
<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>
<ButtonStyled type="transparent" size="standard">
<button @click="editDependencies">
<EditIcon />
Edit
</button>
</ButtonStyled>
</div>
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
</div>
<ButtonStyled type="transparent" size="standard">
<button @click="addDependency">
<PlusIcon />
Add dependency
</button>
</ButtonStyled>
</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 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>
<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 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>
</div>
</template>
@@ -203,14 +194,15 @@
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { EditIcon, getLoaderIcon, UnknownIcon } from '@modrinth/assets'
import { EditIcon, getLoaderIcon, PlusIcon, UnknownIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
ENVIRONMENTS_COPY,
FormattedTag,
injectProjectPageContext,
NavTabs,
Tabs,
type TabsTab,
TagItem,
useVIntl,
} from '@modrinth/ui'
@@ -239,18 +231,14 @@ const { projectV2 } = injectProjectPageContext()
const generatedState = useGeneratedState()
const loaders = computed(() => generatedState.value.loaders)
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
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 setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
}
const isModpack = computed(() => projectType.value === 'modpack')
@@ -279,7 +267,7 @@ const editEnvironment = () => {
const editFiles = () => {
modal.value?.setStage('from-details-files')
}
const editDependencies = () => {
const addDependency = () => {
modal.value?.setStage('from-details-dependencies')
}
@@ -1,5 +1,5 @@
<template>
<div class="shadow-card rounded-2xl border border-solid border-surface-5 bg-surface-3 p-4">
<div class="shadow-card rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<NuxtLink
@@ -107,8 +107,8 @@
<LinkIcon />
</button>
</ButtonStyled>
<ButtonStyled v-tooltip="'Begin review'" circular color="orange">
<button @click="openProjectForReview">
<ButtonStyled circular color="orange">
<button v-tooltip="'Begin review'" @click="openProjectForReview">
<ScaleIcon />
</button>
</ButtonStyled>
@@ -143,7 +143,7 @@
:expand-text="expandText"
collapse-text="Collapse thread"
>
<div class="bg-surface-2 p-4 pt-2">
<div class="bg-surface-2 pt-2">
<ThreadView
v-if="threadWithReportBody"
ref="reportThread"
@@ -28,6 +28,7 @@ import {
injectNotificationManager,
OverflowMenu,
type OverflowMenuOption,
useFormatBytes,
useFormatDateTime,
} from '@modrinth/ui'
import { NavTabs } from '@modrinth/ui'
@@ -56,6 +57,7 @@ const formatDateTimeUtc = useFormatDateTime({
timeZoneName: 'short',
timeZone: 'UTC',
})
const formatBytes = useFormatBytes()
type FlattenedFileReport = Labrinth.TechReview.Internal.FileReport & {
id: string
@@ -362,12 +364,6 @@ 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'
@@ -851,7 +847,7 @@ const reviewSummaryPreview = computed(() => {
const fileVerdict = fileUnsafe > 0 ? 'Unsafe' : 'Safe'
markdown += `### ${fileData.fileName}\n`
markdown += `> ${formatFileSize(fileData.fileSize)}${fileData.decisions.length} issues • Max severity: ${fileData.maxSeverity} • **Verdict:** ${fileVerdict}\n\n`
markdown += `> ${formatBytes(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`
@@ -1079,6 +1075,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
mode="local"
:links="navTabsLinks"
:active-index="activeTabIndex"
class="bg-surface-3! shadow-none!"
@tab-click="handleTabClick"
/>
</div>
@@ -1091,7 +1088,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 p-4 pt-0">
<div class="bg-surface-2 pt-0">
<!-- DEV-531 -->
<!-- @vue-expect-error TODO: will convert ThreadView to use api-client types at a later date -->
<ThreadView
@@ -1150,7 +1147,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">{{
formatFileSize(file.file_size)
formatBytes(file.file_size)
}}</span>
</div>
<div
@@ -358,26 +358,44 @@
<div v-else-if="generatedMessage" class="flex items-center gap-2">
<ButtonStyled>
<button @click="goBackToStages">
<button :disabled="loadingModerationDecision" @click="goBackToStages">
<LeftArrowIcon aria-hidden="true" />
Edit
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="sendMessage('rejected')">
<XIcon aria-hidden="true" />
<button :disabled="loadingModerationDecision" @click="sendMessage('rejected')">
<SpinnerIcon
v-if="moderationDecision === 'rejected'"
class="animate-spin"
aria-hidden="true"
/>
<XIcon v-else aria-hidden="true" />
Reject
</button>
</ButtonStyled>
<ButtonStyled color="orange">
<button @click="sendMessage('withheld')">
<EyeOffIcon aria-hidden="true" />
<button :disabled="loadingModerationDecision" @click="sendMessage('withheld')">
<SpinnerIcon
v-if="moderationDecision === 'withheld'"
class="animate-spin"
aria-hidden="true"
/>
<LinkIcon v-else aria-hidden="true" />
Withhold
</button>
</ButtonStyled>
<ButtonStyled color="green">
<button @click="sendMessage(projectV2.requested_status ?? 'approved')">
<CheckIcon aria-hidden="true" />
<button
:disabled="loadingModerationDecision"
@click="sendMessage(approveSendStatus)"
>
<SpinnerIcon
v-if="moderationDecision === approveSendStatus"
class="animate-spin"
aria-hidden="true"
/>
<CheckIcon v-else aria-hidden="true" />
Approve
</button>
</ButtonStyled>
@@ -428,14 +446,15 @@ import {
BrushCleaningIcon,
CheckIcon,
DropdownIcon,
EyeOffIcon,
FileTextIcon,
KeyboardIcon,
LeftArrowIcon,
LinkIcon,
ListBulletedIcon,
LockIcon,
RightArrowIcon,
ScaleIcon,
SpinnerIcon,
ToggleLeftIcon,
ToggleRightIcon,
XIcon,
@@ -760,6 +779,12 @@ 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() {
@@ -1074,6 +1099,7 @@ 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}`)
@@ -1190,7 +1216,7 @@ function handleKeybinds(event: KeyboardEvent) {
tryResetProgress: resetProgress,
tryExitModeration: handleExit,
tryApprove: () => sendMessage(projectV2.value.requested_status ?? 'approved'),
tryApprove: () => sendMessage(approveSendStatus.value),
tryReject: () => sendMessage('rejected'),
tryWithhold: () => sendMessage('withheld'),
tryEditMessage: goBackToStages,
@@ -1977,6 +2003,7 @@ async function sendMessage(status: ProjectStatus) {
return
}
moderationDecision.value = status
try {
await useBaseFetch(`project/${projectId}`, {
method: 'PATCH',
@@ -2026,6 +2053,8 @@ async function sendMessage(status: ProjectStatus) {
text: 'Failed to submit moderation decision. Please try again.',
type: 'error',
})
} finally {
moderationDecision.value = null
}
}
@@ -1,17 +1,31 @@
<template>
<div>
<section class="universal-card">
<section>
<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" />
<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"
/>
</section>
<section v-if="report && thread" class="universal-card">
<h2>Messages</h2>
<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>
<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,28 +1,45 @@
<template>
<div>
<Modal
<NewModal
ref="modalSubmit"
:header="isRejected(project) ? 'Resubmit for review' : 'Submit for review'"
:header="
formatMessage(
isRejected(project)
? messages.resubmitModalHeaderResubmitting
: messages.resubmitModalHeaderSubmitting,
)
"
>
<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>
<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>
<Checkbox
v-model="submissionConfirmation"
description="Confirm I have addressed the messages from the moderators"
:description="formatMessage(messages.resubmitModalConfirmationDescription)"
>
I confirm that I have properly addressed the moderators' comments.
{{ formatMessage(messages.resubmitModalConfirmationLabel) }}
</Checkbox>
<div class="input-group push-right">
<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>
<ButtonStyled color="orange">
<button
:disabled="!submissionConfirmation || isLoading"
@@ -34,33 +51,37 @@
aria-hidden="true"
/>
<ScaleIcon v-else aria-hidden="true" />
Resubmit for review
{{ formatMessage(messages.actionResubmitForReview) }}
</button>
</ButtonStyled>
</div>
</div>
</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>
</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>
<Checkbox
v-model="replyConfirmation"
description="Confirm moderators do not actively monitor this"
:description="formatMessage(messages.replyModalConfirmationDescription)"
>
I acknowledge that the moderators do not actively monitor the thread.
{{ formatMessage(messages.replyModalConfirmationLabel) }}
</Checkbox>
<div class="input-group push-right">
<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>
<ButtonStyled color="brand">
<button
:disabled="!replyConfirmation || isLoading"
@@ -72,289 +93,318 @@
aria-hidden="true"
/>
<ReplyIcon v-else aria-hidden="true" />
Reply to thread
{{ formatMessage(messages.actionReplyToThread) }}
</button>
</ButtonStyled>
</div>
</div>
</Modal>
<div v-if="flags.developerMode" class="thread-id">
</NewModal>
<div v-if="flags.developerMode" class="mx-4 mb-3 font-semibold">
Thread ID:
<CopyCode :text="thread.id" />
</div>
<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 :disabled="isLoading" @click="runBlockingAction('reopen', () => reopenReport())">
<SpinnerIcon v-if="loadingAction === 'reopen'" class="animate-spin" aria-hidden="true" />
<CheckCircleIcon v-else 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 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>
<div class="input-group">
<ButtonStyled color="brand">
<button
v-if="sortedMessages.length > 0"
:disabled="!replyBody || isLoading"
@click="
isApproved(project) && !isStaff(auth.user)
? openReplyModal()
: runBlockingAction('reply', () => sendReply())
"
>
<SpinnerIcon v-if="loadingAction === 'reply'" class="animate-spin" aria-hidden="true" />
<ReplyIcon v-else aria-hidden="true" />
Reply
</button>
<button
v-else
:disabled="!replyBody || isLoading"
@click="
isApproved(project) && !isStaff(auth.user)
? openReplyModal()
: runBlockingAction('send', () => sendReply())
"
>
<SpinnerIcon v-if="loadingAction === 'send'" class="animate-spin" aria-hidden="true" />
<SendIcon v-else aria-hidden="true" />
Send
</button>
</ButtonStyled>
<template v-if="report && report.closed">
<p>{{ formatMessage(messages.closedThreadDescription) }}</p>
<ButtonStyled v-if="isStaff(auth.user)">
<button
:disabled="!replyBody || isLoading"
@click="runBlockingAction('private-note', () => sendReply(null, true))"
>
<button :disabled="isLoading" @click="runBlockingAction('reopen', () => reopenReport())">
<SpinnerIcon
v-if="loadingAction === 'private-note'"
v-if="loadingAction === 'reopen'"
class="animate-spin"
aria-hidden="true"
/>
<ScaleIcon v-else aria-hidden="true" />
Add private note
<CheckCircleIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionReopenThread) }}
</button>
</ButtonStyled>
<template v-if="currentMember && !isStaff(auth.user)">
<template v-if="isRejected(project)">
<ButtonStyled color="orange">
<button v-if="replyBody" :disabled="isLoading" @click="openResubmitModal(true)">
<ScaleIcon aria-hidden="true" />
Resubmit for review with reply
</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) }}
</button>
<button v-else :disabled="isLoading" @click="openResubmitModal(false)">
<ScaleIcon aria-hidden="true" />
Resubmit for review
<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>
</ButtonStyled>
</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"
: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" />
Close with reply
</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" />
Close thread
</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) }}
</button>
<button v-else :disabled="isLoading" @click="openResubmitModal(false)">
<ScaleIcon aria-hidden="true" />
{{ formatMessage(messages.actionResubmitForReview) }}
</button>
</ButtonStyled>
</template>
</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))"
>
<SpinnerIcon
v-if="loadingAction === 'approve-with-reply'"
class="animate-spin"
aria-hidden="true"
/>
<CheckIcon v-else aria-hidden="true" />
Approve with reply
</button>
</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" />
Approve
</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" />
Reject with reply
</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" />
Reject
</button>
</ButtonStyled>
</div>
<div class="flex flex-wrap items-center gap-2">
<template v-if="report">
<template v-if="isStaff(auth.user)">
<ButtonStyled color="red">
<OverflowMenu
class="btn-dropdown-animation"
<button
v-if="replyBody"
: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,
},
]
@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))
"
>
<SpinnerIcon v-if="isDropdownLoading" class="animate-spin" aria-hidden="true" />
<DropdownIcon v-else 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>
<SpinnerIcon
v-if="loadingAction === 'approve-with-reply'"
class="animate-spin"
aria-hidden="true"
/>
<CheckIcon v-else aria-hidden="true" />
{{ formatMessage(messages.actionApproveWithReply) }}
</button>
</ButtonStyled>
</div>
<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>
</template>
</template>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
</template>
@@ -374,19 +424,179 @@ import {
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: {
@@ -532,7 +742,7 @@ async function sendReply(status = null, privateMessage = false) {
}
} catch (err) {
addNotification({
title: 'Error sending message',
title: formatMessage(messages.errorSendingMessage),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -554,7 +764,7 @@ async function closeReport(reply) {
await updateThreadLocal()
} catch (err) {
addNotification({
title: 'Error closing report',
title: formatMessage(messages.errorClosingReport),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -572,7 +782,7 @@ async function reopenReport() {
await updateThreadLocal()
} catch (err) {
addNotification({
title: 'Error reopening report',
title: formatMessage(messages.errorReopeningReport),
text: err.data ? err.data.description : err,
type: 'error',
})
@@ -604,44 +814,8 @@ 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,10 +1,11 @@
<template>
<div
class="message"
class="message px-4 py-3"
: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]">
@@ -22,11 +23,6 @@
/>
</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>
@@ -35,6 +31,11 @@
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'"
@@ -79,6 +80,12 @@
<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" />.
@@ -126,7 +133,7 @@
<script setup>
import {
LockIcon,
EyeOffIcon,
MicrophoneIcon,
ModrinthIcon,
MoreHorizontalIcon,
@@ -178,6 +185,7 @@ const props = defineProps({
})
const emit = defineEmits(['update-thread'])
const flags = useFeatureFlags()
const formattedMessage = computed(() => {
const body = renderString(props.message.body.body)
@@ -222,15 +230,13 @@ async function deleteMessage() {
<style lang="scss" scoped>
.message {
--gap-size: var(--spacing-card-xs);
display: flex;
flex-direction: row;
gap: var(--gap-size);
gap: var(--spacing-card-sm);
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 {
@@ -238,14 +244,12 @@ 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 {
@@ -260,13 +264,22 @@ async function deleteMessage() {
&:not(.no-actions):hover,
&:not(.no-actions):focus-within {
background-color: var(--color-table-alternate-row);
background-color: var(--surface-2-5);
.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;
@@ -346,10 +359,6 @@ a:active + .message__author a,
color: var(--color-purple);
}
.private-icon {
color: var(--color-gray);
}
@media screen and (min-width: 600px) {
.message {
//grid-template:
@@ -363,6 +372,7 @@ 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;
}
}
}
@@ -377,7 +387,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 auto;
grid-template-rows: min-content 1fr;
}
}
}
@@ -1,11 +1,11 @@
<template>
<div>
<div v-if="flags.developerMode" class="mt-4 font-bold text-heading">
<div v-if="flags.developerMode" class="m-4 font-bold text-heading">
Thread ID:
<CopyCode :text="thread.id" />
</div>
<div v-if="sortedMessages.length > 0" class="flex flex-col space-y-4 rounded-xl p-3 sm:p-4">
<div v-if="sortedMessages.length > 0" class="flex flex-col rounded-xl">
<ThreadMessage
v-for="message in sortedMessages"
:key="'message-' + message.id"
@@ -28,7 +28,7 @@
</template>
<template v-else>
<div>
<div class="px-4 py-2">
<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 sm:flex-row sm:items-center sm:gap-2"
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"
>
<div class="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center">
<ButtonStyled v-if="sortedMessages.length > 0" color="brand">
@@ -49,6 +49,11 @@ 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
@@ -0,0 +1,746 @@
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,
}
}
-32
View File
@@ -1,32 +0,0 @@
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,26 +28,44 @@ export function createLoaderParsers(
if (metadata.dependencies) {
const neoForgeDependency = Object.values(metadata.dependencies)
.flat()
.find((dependency: any) => dependency.modId === 'neoforge')
.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)
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
}
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
},
)
}
}
@@ -25,6 +25,7 @@ export function getGameVersionsMatchingSemverRange(
export function getGameVersionsMatchingMavenRange(
range: string | undefined,
gameVersions: string[],
processor: (version: string) => string | null = (v) => v,
): string[] {
if (!range) {
return []
@@ -63,24 +64,42 @@ export function getGameVersionsMatchingMavenRange(
for (const range of ranges) {
let result
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]}`)
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}`)
}
}
return getGameVersionsMatchingSemverRange(semverRanges, gameVersions)
+23 -8
View File
@@ -34,25 +34,39 @@
'modrinth-parent__no-modal-blurs': !cosmetics.advancedRendering,
}"
>
<RussiaBanner v-if="isRussia" />
<TaxIdMismatchBanner v-if="showTinMismatchBanner" />
<TaxComplianceBanner v-if="showTaxComplianceBanner" />
<RussiaBanner v-if="flags.showAllBanners || isRussia" />
<TaxIdMismatchBanner v-if="flags.showAllBanners || showTinMismatchBanner" />
<TaxComplianceBanner v-if="flags.showAllBanners || showTaxComplianceBanner" />
<VerifyEmailBanner
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
v-if="
flags.showAllBanners ||
(auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email')
"
:has-email="!!auth?.user?.email"
/>
<SubscriptionPaymentFailedBanner
v-if="
user.subscriptions.some((x) => x.status === 'payment-failed') &&
route.path !== '/settings/billing'
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')
"
/>
<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]"
>
@@ -767,6 +781,7 @@ 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'
+6 -3
View File
@@ -506,6 +506,12 @@
"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"
},
@@ -944,9 +950,6 @@
"moderation.page.reports": {
"message": "Hlášení"
},
"moderation.page.technicalReview": {
"message": "Technické hodnocení"
},
"muralpay.country.at": {
"message": "Rakousko"
},
@@ -1229,6 +1229,33 @@
"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."
},
@@ -1244,6 +1271,12 @@
"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."
},
@@ -1670,6 +1703,9 @@
"layout.action.create-new": {
"message": "Erstellen..."
},
"layout.action.external-projects": {
"message": "Externe Projekte"
},
"layout.action.file-lookup": {
"message": "Datei ansehen"
},
@@ -1922,6 +1958,9 @@
"moderation.moderate": {
"message": "Moderieren"
},
"moderation.page.external-projects": {
"message": "Externe Projekte"
},
"moderation.page.projects": {
"message": "Projekte"
},
@@ -2615,6 +2654,45 @@
"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"
},
@@ -2633,6 +2711,15 @@
"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"
},
@@ -3002,6 +3089,9 @@
"search.filter.locked.server.sync": {
"message": "Mit Server synchronisieren"
},
"servers.manage.content.title": {
"message": "Inhalt - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Aktionen"
},
@@ -1229,6 +1229,27 @@
"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."
},
@@ -1244,6 +1265,12 @@
"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."
},
@@ -1670,6 +1697,9 @@
"layout.action.create-new": {
"message": "Erstellen..."
},
"layout.action.external-projects": {
"message": "Externe Projekte"
},
"layout.action.file-lookup": {
"message": "Dateisuche"
},
@@ -1922,6 +1952,9 @@
"moderation.moderate": {
"message": "Moderieren"
},
"moderation.page.external-projects": {
"message": "Externe Projekte"
},
"moderation.page.projects": {
"message": "Projekte"
},
@@ -2615,6 +2648,45 @@
"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"
},
@@ -2633,6 +2705,15 @@
"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"
},
@@ -3002,6 +3083,9 @@
"search.filter.locked.server.sync": {
"message": "Mit Server synchronisieren"
},
"servers.manage.content.title": {
"message": "Inhalt - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Aktionen"
},
+248 -20
View File
@@ -407,6 +407,117 @@
"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."
},
@@ -875,23 +986,8 @@
"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 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"
"message": "You have no unread notifications."
},
"dashboard.notifications.link.see-all": {
"message": "See all"
@@ -902,9 +998,6 @@
"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"
},
@@ -920,6 +1013,27 @@
"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."
},
@@ -1229,6 +1343,33 @@
"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."
},
@@ -1727,14 +1868,20 @@
"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 <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}."
"message": "If you meant to access the official Modrinth website, visit {url}. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}."
},
"layout.banner.preview.title": {
"message": "This is a preview deploy of the Modrinth website."
@@ -2564,6 +2711,84 @@
"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"
},
@@ -3062,6 +3287,9 @@
"search.filter.locked.server.sync": {
"message": "Sync with server"
},
"servers.manage.content.title": {
"message": "Content - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Actions"
},
+69 -9
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": "Proyectos de servidor no soportan edición masiva"
"message": "Los proyectos no son compatible con la edición masiva de servidores"
},
"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": "Cambios seran a aplicados a {count} {count, plural,one {proyecto} other {proyectos}}."
"message": "Los cambios se aplicaran en <strong>{count}</strong>{count, plural,one {proyecto}other {proyectos}}."
},
"dashboard.projects.links.description": {
"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."
"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."
},
"dashboard.projects.links.discord-invite.description": {
"message": "Un link de invitación a tu servidor de Discord."
"message": "Una 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 para usuarios para reportar bugs, errores, y preocupaciones sobre tu proyecto."
"message": "Un lugar donde los usuarios reporten errores, problemas 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": "Edición masiva seleccionada de tus links de proyecto."
"message": "Se editaron los enlaces de los proyectos seleccionados."
},
"dashboard.projects.project.icon-alt": {
"message": "Icono de {title}"
@@ -1244,6 +1244,12 @@
"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."
},
@@ -1659,7 +1665,7 @@
"message": "Para jugadores"
},
"landing.section.for-players.tagline": {
"message": "Descubre sobre las creaciones de {count, number}"
"message": "Descubre s de {count, number} creaciones"
},
"landing.subheading": {
"message": "Descubre, juega y comparte contenido de Minecraft a través de nuestra plataforma de código abierto creada para la comunidad."
@@ -1670,6 +1676,9 @@
"layout.action.create-new": {
"message": "Crear nuevo..."
},
"layout.action.external-projects": {
"message": "Proyectos externos"
},
"layout.action.file-lookup": {
"message": "Buscar archivo"
},
@@ -1922,6 +1931,9 @@
"moderation.moderate": {
"message": "Moderar"
},
"moderation.page.external-projects": {
"message": "Proyectos externos"
},
"moderation.page.projects": {
"message": "Proyectos"
},
@@ -2615,6 +2627,45 @@
"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"
},
@@ -2633,14 +2684,23 @@
"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 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."
"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."
},
"report.back-to-item": {
"message": "Volver a {item, select, project {proyecto} version {version} user {usuario} other {contenido}}"
"message": "Volver {item, select, project {al proyecto} version {a la versión} user {al usuario} other {al 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,9 +1928,6 @@
"moderation.page.reports": {
"message": "Informes"
},
"moderation.page.technicalReview": {
"message": "Revisión Técnica"
},
"muralpay.account-type.checking": {
"message": "Cheque"
},
+130 -10
View File
@@ -393,7 +393,7 @@
"message": "Wala pang proyekto sa koleksiyon"
},
"collection.label.projects-count": {
"message": "{count, plural, =0 {Wala pang proyekto} one {<stat>{count}</stat> {type}} other {<stat>{count}</stat> na {type}}}"
"message": "{count, plural, =0 {Wala pang proyekto} other {<stat>{count}</stat> {type}}}"
},
"collection.label.updated-at": {
"message": "In-update {ago}"
@@ -554,6 +554,9 @@
"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"
},
@@ -572,6 +575,15 @@
"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"
},
@@ -591,11 +603,23 @@
"message": "{count} {countPlural, plural, one {proyekto} other {na proyekto}}"
},
"dashboard.collections.label.search-input": {
"message": "Hanapin ang iyong mga koleksiyon"
"message": "Maghanap sa iyong 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}"
},
@@ -848,6 +872,66 @@
"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"
},
@@ -884,6 +968,9 @@
"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"
},
@@ -896,6 +983,9 @@
"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."
},
@@ -908,6 +998,9 @@
"dashboard.revenue.withdraw.header": {
"message": "Mag-withdraw"
},
"dashboard.sidebar.label.activeReports": {
"message": "Aktibong sumbong"
},
"dashboard.withdraw.completion.account": {
"message": "Account"
},
@@ -957,7 +1050,7 @@
"message": "Kailangang mapatunayan ang email"
},
"dashboard.withdraw.error.generic.text": {
"message": "Hindi namin masumite ang iyong hiling na mag-withdraw, mangyaring suriin ang iyong mga detalye o abutin ang suporta. {error}"
"message": "Hindi namin maisumite 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"
@@ -990,7 +1083,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 halaga"
"message": "Labis na mababa ang balyu"
},
"dashboard.withdraw.error.paypal-country-mismatch.text": {
"message": "Mangyaring pillin ang wastong PayPal transfer option para sa iyong rehiyon (US o internasyunal)."
@@ -1497,7 +1590,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": "May error sa pagbuo ng estado galing sa API habang nagpapatayo."
"message": "Error sa pagbubuo 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}."
@@ -1704,7 +1797,7 @@
"message": "Mga Sumbong"
},
"moderation.page.technicalReview": {
"message": "Teknikal na Pagsusuri"
"message": "Tech review"
},
"muralpay.account-type.checking": {
"message": "Sinusuri"
@@ -2241,7 +2334,7 @@
"message": "Bersiyon ng laro: {version}"
},
"project.download.game-version-error": {
"message": "Kamalian: walang mahanap na mga bersiyon ng laro"
"message": "Error: walang mahanap na mga bersiyon ng laro"
},
"project.download.game-version-tooltip": {
"message": "Ang {title} ay magagamit lamang sa {version}"
@@ -2262,7 +2355,7 @@
"message": "Plataporma: {platform}"
},
"project.download.platform-error": {
"message": "Kamalian: walang mahanap na platform"
"message": "Error: walang mahanap na platform"
},
"project.download.platform-tooltip": {
"message": "Ang {title} ay magagamit lamang sa {platform}"
@@ -2307,7 +2400,7 @@
"message": "Pakisuri sa metadata sa environment"
},
"project.error.loading": {
"message": "Nagkamali sa pag-load ng project data{message}"
"message": "Error sa paglo-load ng datos ng proyekto{message}"
},
"project.error.page-not-found": {
"message": "Hindi mahanap na pahina"
@@ -2390,6 +2483,9 @@
"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"
},
@@ -2816,6 +2912,9 @@
"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"
},
@@ -2831,6 +2930,9 @@
"settings.account.security.email.title": {
"message": "Email"
},
"settings.account.security.password.action.add": {
"message": "Magdagdag ng password"
},
"settings.account.security.password.title": {
"message": "Password"
},
@@ -2918,6 +3020,9 @@
"settings.applications.field.url.placeholder": {
"message": "https://example.com"
},
"settings.applications.head-title": {
"message": "Mga aplikasyon"
},
"settings.applications.modal.header": {
"message": "Impormasyon ng aplikasyon"
},
@@ -2940,7 +3045,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"
@@ -3014,6 +3119,15 @@
"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."
},
@@ -3125,6 +3239,9 @@
"settings.display.theme.title": {
"message": "Paksang kulay"
},
"settings.head-title": {
"message": "Settings ng display"
},
"settings.pats.action.create": {
"message": "Lumikha ng PAT"
},
@@ -3185,6 +3302,9 @@
"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"
},
+99 -9
View File
@@ -369,7 +369,7 @@
"message": "Cela va supprimer de manière permanente cette collection. Cette action est irréversible."
},
"collection.delete-modal.title": {
"message": "Êtes-vous sûr.e de vouloir supprimer cette collection ?"
"message": "Êtes-vous sûr·e de vouloir supprimer cette collection ?"
},
"collection.description": {
"message": "{description} - Voir la collection {name} par {username} sur Modrinth"
@@ -570,7 +570,7 @@
"message": "Révoquer"
},
"dashboard.affiliate-links.revoke-confirm.title": {
"message": "Êtes-vous sûr.e de vouloir révoquer votre lien d'affiliation « {title} » ?"
"message": "Êtes-vous sûr·e de vouloir révoquer votre lien d'affiliation « {title} » ?"
},
"dashboard.affiliate-links.search": {
"message": "Rechercher des liens d'affiliation..."
@@ -654,7 +654,7 @@
"message": "Pratiques de sécurité"
},
"dashboard.creator-tax-form-modal.us-citizen.question": {
"message": "Êtes-vous un.e citoyen(ne) des États-Unis d'Amérique ?"
"message": "Êtes-vous un·e citoyen(ne) des États-Unis d'Amérique ?"
},
"dashboard.creator-withdraw-modal.complete-tax-form": {
"message": "Remplissez le formulaire fiscal"
@@ -1229,6 +1229,33 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Veuillez remplir le formulaire fiscal"
},
"discover.install.back-to-server": {
"message": "Retour au serveur"
},
"discover.install.back-to-setup": {
"message": "Retour au setup"
},
"discover.install.cancel-reset": {
"message": "Annuler la réinitialisation"
},
"discover.install.error.no-server-world": {
"message": "Aucun monde serveur n'est disponible pour l'installation."
},
"discover.install.error.unsupported-content-type": {
"message": "Ce type de contenu ne peut pas être installé sur un serveur depuis la navigation."
},
"discover.install.heading.reset-modpack": {
"message": "Sélectionner un modpack à insaller après la réinitialisation"
},
"discover.seo.description": {
"message": "Recherchez et parcourez des milliers de {projectType, select, mod {mods} modpack {modpacks} resourcepack {packs de ressources} shader {shaders} plugin {plugins} datapack {packs de données} other {projets}} Minecraft sur Modrinth avec des résultars fidèles et rapides. Nos filtres vous aident à trouver les meilleurs {projectType, select, mod {mods} modpack {modpacks} resourcepack {packs de ressources} shader {shaders} plugin {plugins} datapack {packs de données} other {projets}}."
},
"discover.seo.title": {
"message": "Rechercher des {projectType, select, mod {mods} modpack {modpacks} resourcepack {packs de ressources} shader {shaders} plugin {plugins} datapack {packs de données} other {projets}}"
},
"discover.seo.title-with-query": {
"message": "Search {projectType, select, mod {mods} modpack {modpacks} resourcepack {packs de ressources} shader {shaders} plugin {plugins} datapack {packs de données} other {projets}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Vous avez peut-être mal tapé l'URL de la collection."
},
@@ -1244,6 +1271,12 @@
"error.collection.404.title": {
"message": "Collection introuvable"
},
"error.generic.401.signed-in-as": {
"message": "Vous êtes actuellement connecté sous le nom de :"
},
"error.generic.401.title": {
"message": "Vous n'avez pas accès à cette page"
},
"error.generic.404.subtitle": {
"message": "La page que vous recherchez ne semble pas exister."
},
@@ -1653,7 +1686,7 @@
"message": "Partagez vos créations avec le monde"
},
"landing.section.for-players.description": {
"message": "Des biomes magiques aux donjons maudits, vous pouvez être sûr.e de trouver ce que vous cherchez pour amener votre gameplay au niveau supérieur."
"message": "Des biomes magiques aux donjons maudits, vous pouvez être sûr·e de trouver ce que vous cherchez pour amener votre gameplay au niveau supérieur."
},
"landing.section.for-players.label": {
"message": "Pour les joueurs"
@@ -1670,6 +1703,9 @@
"layout.action.create-new": {
"message": "Créer nouveau..."
},
"layout.action.external-projects": {
"message": "Projets externes"
},
"layout.action.file-lookup": {
"message": "Recherche de fichier"
},
@@ -1922,6 +1958,9 @@
"moderation.moderate": {
"message": "Modérer"
},
"moderation.page.external-projects": {
"message": "Projets externes"
},
"moderation.page.projects": {
"message": "Projets"
},
@@ -2615,6 +2654,45 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.attention-needed.description.proj-approved": {
"message": "Veuillez prouver que vous avez la permission de redistribuer tous les fichiers suivants et que toutes les versions retenues seront automatiquement publiées."
},
"project.settings.permissions.attention-needed.description.proj-draft": {
"message": "Veuillez fournir la preuve que vous avez l'autorisation de redistribuer tous les fichiers suivants avant de pouvoir soumettre votre projet pour évaluation."
},
"project.settings.permissions.attention-needed.title": {
"message": "Contenu intégré inconnu"
},
"project.settings.permissions.completed.description": {
"message": "Tout le contenu externe est accompagné d'une attribution."
},
"project.settings.permissions.completed.title": {
"message": "Attributions terminées !"
},
"project.settings.permissions.empty-state.description": {
"message": "Aucune de vos versions ne contient de contenu externe, vous n'avez donc pas à vous soucier d'obtenir d'autorisations."
},
"project.settings.permissions.empty-state.heading": {
"message": "Vous êtes prêt !"
},
"project.settings.permissions.fail.description": {
"message": "Vous n'êtes pas autorisé à rediffuser certains des contenus externes que vous avez ajoutés. Pour pouvoir publier sur Modrinth, veuillez supprimer les contenus litigieux."
},
"project.settings.permissions.fail.title": {
"message": "Certains contenus ne peuvent pas être inclus"
},
"project.settings.permissions.info-banner.description": {
"message": "Si vous incluez du contenu qui nest pas sur Modrinth, vous devez nous faire savoir doù il vient et vérifier que vous avez la permission de distribuer les fichiers. Jettez un coup d’œil à <link>notre guide</link> pour apprendre comment faire cela proprement !"
},
"project.settings.permissions.info-banner.title": {
"message": "Découvrez comment fonctionnent les attributions"
},
"project.settings.permissions.learn-more": {
"message": "En savoir plus"
},
"project.settings.permissions.search-placeholder": {
"message": "Rechercher {count} {count, plural, one {projet externe} other {projets externes}}..."
},
"project.settings.title": {
"message": "Paramètres"
},
@@ -2633,6 +2711,15 @@
"project.versions.title": {
"message": "Versions"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, one {Cette version est actuellement indisponible et n'est pas répertoriée} other {Ces versions sont actuellement indisponibles et n'ont pas été répertoriées}} publiquement. Veuillez fournir la preuve que vous avez l'autorisation de redistribuer certains fichiers inclus dans {count, plural, one {la version} other {les versions}} du modpack."
},
"project.versions.withheld-versions-warning.resolve-button": {
"message": "Résoudre"
},
"project.versions.withheld-versions-warning.title": {
"message": "{count, plural, one {Version {version_name} non disponible} other {Versions non disponibles}} en raison de contenus intégrés inconnus"
},
"report.already-reported": {
"message": "Vous avez déjà signalé {title}"
},
@@ -3002,6 +3089,9 @@
"search.filter.locked.server.sync": {
"message": "Synchroniser avec le serveur"
},
"servers.manage.content.title": {
"message": "Contenu - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Actions"
},
@@ -3063,7 +3153,7 @@
"message": "Supprimer ce compte"
},
"settings.account.delete.confirm.title": {
"message": "Êtes-vous sûr.e de vouloir supprimer votre compte ?"
"message": "Êtes-vous sûr·e de vouloir supprimer votre compte ?"
},
"settings.account.delete.section.action": {
"message": "Supprimer le compte"
@@ -3282,7 +3372,7 @@
"message": "Vous êtes sur le point de supprimer cette application et retirer tous les jetons d'accès. (pour toujours !)"
},
"settings.applications.delete.confirm.title": {
"message": "Êtes-vous sûr.e de vouloir supprimer cette application ?"
"message": "Êtes-vous sûr·e de vouloir supprimer cette application ?"
},
"settings.applications.description.intro": {
"message": "Les applications permettent d'authentifier les utilisateurs de Modrinth auprès de vos produits. Pour plus d'informations, consultez la <docs-link>documentation de l'API Modrinth</docs-link>."
@@ -3354,7 +3444,7 @@
"message": "Cela révoquera laccès de lapplication à votre compte. Vous pourrez toujours la réautoriser plus tard."
},
"settings.authorizations.revoke.confirm.title": {
"message": "Êtes-vous sûr.e de vouloir révoquer cette application?"
"message": "Êtes-vous sûr·e de vouloir révoquer cette application?"
},
"settings.billing.charges.description": {
"message": "Toutes vos transactions passées sur votre compte Modrinth seront affichées ici :"
@@ -3432,7 +3522,7 @@
"message": "Cela annulera votre abonnement. Vous conserverez vos avantages jusqu’à la fin du cycle de facturation en cours."
},
"settings.billing.modal.cancel.title": {
"message": "Êtes-vous sûr.e de vouloir annuler votre abonnement?"
"message": "Êtes-vous sûr·e de vouloir annuler votre abonnement?"
},
"settings.billing.modal.delete.action": {
"message": "Supprimer ce mode de paiement"
@@ -3441,7 +3531,7 @@
"message": "Cela supprimera définitivement ce mode paiement. (vraiment toujours)."
},
"settings.billing.modal.delete.title": {
"message": "Êtes-vous sûr.e de vouloir supprimer ce mode de paiement ?"
"message": "Êtes-vous sûr·e de vouloir supprimer ce mode de paiement ?"
},
"settings.billing.next": {
"message": "Suivant :"
+8 -5
View File
@@ -381,7 +381,7 @@
"message": "Gyűjtemény szerkesztése"
},
"collection.error.not-found": {
"message": "Nncs ilyen gyűjtemény"
"message": "Nincs ilyen gyűjtemény"
},
"collection.label.created-at": {
"message": "Létrehozva {ago}"
@@ -605,6 +605,9 @@
"dashboard.collections.long-title": {
"message": "Gyűjteményeid"
},
"dashboard.collections.placeholder.search": {
"message": "Gyűjtemények keresése"
},
"dashboard.collections.sort.name-ascending": {
"message": "Név (A-Z)"
},
@@ -1205,6 +1208,9 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Töltsd ki az adóbevallási űrlapot"
},
"discover.install.back-to-server": {
"message": "Vissza a szerverhez"
},
"error.collection.404.list_item.1": {
"message": "Elírhatad a gyűjtemémyed linkjét."
},
@@ -1848,7 +1854,7 @@
"message": "Menü megnyitása"
},
"layout.nav.active-reports": {
"message": "Aktív Bejelentések"
"message": "Aktív jelentések"
},
"layout.nav.discover": {
"message": "Felfedezés"
@@ -1904,9 +1910,6 @@
"moderation.page.reports": {
"message": "Jelentések"
},
"moderation.page.technicalReview": {
"message": "Műszaki áttekintés"
},
"muralpay.account-type.checking": {
"message": "Ellenőrzés"
},
@@ -1703,9 +1703,6 @@
"moderation.page.reports": {
"message": "Laporan"
},
"moderation.page.technicalReview": {
"message": "Tinjauan Teknis"
},
"muralpay.account-type.checking": {
"message": "Giro"
},
+118 -28
View File
@@ -264,7 +264,7 @@
"message": "Inserisci un codice 2FA"
},
"auth.sign-in.2fa.placeholder": {
"message": "Inserisci codice..."
"message": "Inserisci il codice..."
},
"auth.sign-in.additional-options": {
"message": "<forgot-password-link>Password dimenticata?</forgot-password-link> • <create-account-link>Crea un account</create-account-link>"
@@ -360,7 +360,7 @@
"message": "Cambia icona"
},
"collection.button.select-icon": {
"message": "Seleziona icona"
"message": "Scegli un'icona"
},
"collection.button.unfollow-project": {
"message": "Smetti di seguire il progetto"
@@ -573,7 +573,7 @@
"message": "Vuoi davvero revocare il tuo link affiliati \"{title}\"?"
},
"dashboard.affiliate-links.search": {
"message": "Cerca link affiliati..."
"message": "Cerca tra i link affiliati..."
},
"dashboard.analytics.from-projects": {
"message": "da {count, plural, one {# progetto} other {# progetti}}"
@@ -591,7 +591,7 @@
"message": "Inizia creando la tua prima raccolta!"
},
"dashboard.collections.empty.no-collections": {
"message": "Non hai ancora alcuna raccolta"
"message": "Ancora nessuna raccolta"
},
"dashboard.collections.empty.no-match": {
"message": "Nessuna raccolta corrisponde alla tua ricerca"
@@ -603,7 +603,7 @@
"message": "{count} {countPlural, plural, one {progetto} other {progetti}}"
},
"dashboard.collections.label.search-input": {
"message": "Cerca fra le tue raccolte"
"message": "Cerca tra le tue raccolte"
},
"dashboard.collections.long-title": {
"message": "Le tue raccolte"
@@ -696,10 +696,10 @@
"message": "Privato"
},
"dashboard.creator-withdraw-modal.method-selection.country-placeholder": {
"message": "Seleziona paese"
"message": "Seleziona il paese"
},
"dashboard.creator-withdraw-modal.method-selection.country-search-placeholder": {
"message": "Cerca paesi..."
"message": "Cerca tra i paesi..."
},
"dashboard.creator-withdraw-modal.method-selection.error-text": {
"message": "Impossibile ottenere i metodi di pagamento. Riprova più tardi."
@@ -714,7 +714,7 @@
"message": "Alcuni metodi di prelievo non sono disponibili in certe regioni."
},
"dashboard.creator-withdraw-modal.method-selection.select-method": {
"message": "Seleziona metodo di prelievo"
"message": "Seleziona il metodo di prelievo"
},
"dashboard.creator-withdraw-modal.method-selection.tax-limit-warning": {
"message": "La tua soglia di prelievo è <b>{amount}</b>, <tax-link>compila un modulo fiscale</tax-link> per prelevare di più."
@@ -837,19 +837,19 @@
"message": "Premio"
},
"dashboard.creator-withdraw-modal.tremendous-details.reward-placeholder": {
"message": "Seleziona premio"
"message": "Seleziona il premio"
},
"dashboard.creator-withdraw-modal.tremendous-details.reward-plural": {
"message": "Premi"
},
"dashboard.creator-withdraw-modal.tremendous-details.search-amount-label": {
"message": "Cerca valore"
"message": "Cerca un valore"
},
"dashboard.creator-withdraw-modal.tremendous-details.select-denomination-hint": {
"message": "Seleziona un taglio:"
},
"dashboard.creator-withdraw-modal.tremendous-details.select-denomination-required": {
"message": "Seleziona un taglio per continuare"
"message": "Seleziona un taglio per procedere"
},
"dashboard.creator-withdraw-modal.tremendous-details.unverified-email-header": {
"message": "Email non verificata"
@@ -909,7 +909,7 @@
"message": "Crea organizzazione"
},
"dashboard.organizations.empty.cta": {
"message": "Crea un'organizzazione!"
"message": "Prova a creare un'organizzazione!"
},
"dashboard.organizations.error.fetch": {
"message": "Impossibile ottenere le organizzazioni"
@@ -927,7 +927,7 @@
"message": "Non puoi modificare più server contemporaneamente"
},
"dashboard.projects.empty": {
"message": "Non hai ancora alcun progetto. Clicca sul pulsante verde in alto per iniziare."
"message": "Ancora nessun progetto. Clicca sul pulsante verde in alto per iniziare."
},
"dashboard.projects.head-title": {
"message": "Progetti"
@@ -1229,6 +1229,33 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Si prega di completare il modulo fiscale"
},
"discover.install.back-to-server": {
"message": "Torna al server"
},
"discover.install.back-to-setup": {
"message": "Torna alla configurazione"
},
"discover.install.cancel-reset": {
"message": "Annulla ripristino"
},
"discover.install.error.no-server-world": {
"message": "Nessun mondo del server può essere installato."
},
"discover.install.error.unsupported-content-type": {
"message": "Questo tipo di contenuto non può essere installato da esplora."
},
"discover.install.heading.reset-modpack": {
"message": "Selezione dei pacchetti da installare dopo il ripristino"
},
"discover.seo.description": {
"message": "Cerca e esplora tra migliaia di {projectType, select, mod {mod} modpack {pacchetti di mod} resourcepack {pacchetti di risorse} shader {shader} plugin {plugin} datapack {pacchetti di dati} other {progetti}} presenti su Modrinth attraverso ricerche rapide e accurate. I nostri filtri ti aiuteranno a trovarne i migliori."
},
"discover.seo.title": {
"message": "Ricerca di {projectType, select, mod {mod} modpack {pacchetti di mod} resourcepack {pacchetti di risorse} shader {shader} plugin {plugin} datapack {pacchetti di dati} other {progetti}}"
},
"discover.seo.title-with-query": {
"message": "Ricerca di {projectType, select, mod {mod} modpack {pacchetti di mod} resourcepack {pacchetti di risorse} shader {shader} plugin {plugin} datapack {pacchetti di dati} other {progetti}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Forse c'è un errore di battitura nell'URL della raccolta."
},
@@ -1244,6 +1271,12 @@
"error.collection.404.title": {
"message": "Raccolta non trovata"
},
"error.generic.401.signed-in-as": {
"message": "Accesso effettuato come:"
},
"error.generic.401.title": {
"message": "Accesso non consentito"
},
"error.generic.404.subtitle": {
"message": "La pagina che cercavi non sembra esistere."
},
@@ -1670,6 +1703,9 @@
"layout.action.create-new": {
"message": "Crea..."
},
"layout.action.external-projects": {
"message": "Progetti esterni"
},
"layout.action.file-lookup": {
"message": "Trova file"
},
@@ -1922,6 +1958,9 @@
"moderation.moderate": {
"message": "Modera"
},
"moderation.page.external-projects": {
"message": "Progetti esterni"
},
"moderation.page.projects": {
"message": "Progetti"
},
@@ -2100,10 +2139,10 @@
"message": "Cellulare"
},
"muralpay.placeholder.account-number": {
"message": "Inserisci codice conto"
"message": "Inserisci il codice del conto"
},
"muralpay.placeholder.cbu-cvu": {
"message": "Inserisci CBU o CVU"
"message": "Inserisci il CBU o CVU"
},
"muralpay.placeholder.cbu-cvu-type": {
"message": "CBU o CVU"
@@ -2112,34 +2151,34 @@
"message": "Inserisci il CCI di 20 cifre"
},
"muralpay.placeholder.cuit-cuil": {
"message": "Inserisci CUIT o CUIL"
"message": "Inserisci il CUIT o CUIL"
},
"muralpay.placeholder.enter-account-number": {
"message": "Inserisci codice conto"
"message": "Inserisci il codice del conto"
},
"muralpay.placeholder.enter-clabe": {
"message": "Inserisci il CLABE di 18 cifre"
},
"muralpay.placeholder.enter-cpf-cnpj": {
"message": "Inserisci CPF o CNPJ"
"message": "Inserisci il CPF o CNPJ"
},
"muralpay.placeholder.enter-iban": {
"message": "Inserisci IBAN"
"message": "Inserisci l'IBAN"
},
"muralpay.placeholder.enter-pix-email": {
"message": "Inserisci email PIX"
"message": "Inserisci l'email PIX"
},
"muralpay.placeholder.enter-random-key": {
"message": "Inserisci chiave casuale"
"message": "Inserisci la chiave casuale"
},
"muralpay.placeholder.enter-routing-number": {
"message": "Inserisci il numero di routing di 9 cifre"
},
"muralpay.placeholder.enter-swift-bic": {
"message": "Inserisci codice SWIFT/BIC"
"message": "Inserisci il codice SWIFT/BIC"
},
"muralpay.placeholder.iban-crc": {
"message": "Inserisci IBAN costaricano"
"message": "Inserisci l'IBAN costaricano"
},
"muralpay.placeholder.phone-cop": {
"message": "+57..."
@@ -2499,13 +2538,13 @@
"message": "Cerca versioni del gioco..."
},
"project.download.search-game-versions-label": {
"message": "Cerca versioni del gioco..."
"message": "Cerca tra le versioni del gioco..."
},
"project.download.select-game-version": {
"message": "Seleziona versione del gioco"
"message": "Seleziona la versione del gioco"
},
"project.download.select-platform": {
"message": "Seleziona piattaforma"
"message": "Seleziona la piattaforma"
},
"project.download.show-all-versions": {
"message": "Mostra tutte le versioni"
@@ -2615,6 +2654,45 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.attention-needed.description.proj-approved": {
"message": "Dimostra di avere il permesso di ridistribuire tutti i seguenti file, e le versioni sospese verranno pubblicate automaticamente."
},
"project.settings.permissions.attention-needed.description.proj-draft": {
"message": "Dimostra di avere il permesso di ridistribuire tutti i seguenti file prima di presentare il tuo progetto alla revisione."
},
"project.settings.permissions.attention-needed.title": {
"message": "Contenuti di origine sconosciuta"
},
"project.settings.permissions.completed.description": {
"message": "Sono state fornite attribuzioni per tutti i contenuti esterni."
},
"project.settings.permissions.completed.title": {
"message": "Attribuzioni complete!"
},
"project.settings.permissions.empty-state.description": {
"message": "Nessuna versione include contenuti esterni, quindi non è necessario dimostrare alcun permesso."
},
"project.settings.permissions.empty-state.heading": {
"message": "Tutto a posto!"
},
"project.settings.permissions.fail.description": {
"message": "Non hai il permesso di ridistribuire alcuni dei contenuti esterni che hai incluso. Per pubblicare su Modrinth, rimuovi i contenuti illeciti."
},
"project.settings.permissions.fail.title": {
"message": "Non puoi includere questi contenuti"
},
"project.settings.permissions.info-banner.description": {
"message": "Se includi dei contenuti esterni a Modrinth, devi dichiarare la loro origine e dimostrare di avere il permesso di ridistribuirli. Controlla la <link>nostra guida</link> per farlo nel modo giusto!"
},
"project.settings.permissions.info-banner.title": {
"message": "Come funzionano le attribuzioni"
},
"project.settings.permissions.learn-more": {
"message": "Scopri di più"
},
"project.settings.permissions.search-placeholder": {
"message": "Cerca tra {count, plural, one {# progetto esterno} other {# progetti esterni}}..."
},
"project.settings.title": {
"message": "Impostazioni"
},
@@ -2633,6 +2711,15 @@
"project.versions.title": {
"message": "Versioni"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, one {Questa versione è attualmente sospesa e non elencata} other {Queste versioni sono attualmente sospese e non elencate}} pubblicamente. Devi dimostrare di avere il permesso di ridistribuire alcuni dei file presenti in {count, plural, one {questa versione} other {queste versioni}} del pacchetto."
},
"project.versions.withheld-versions-warning.resolve-button": {
"message": "Risolvi"
},
"project.versions.withheld-versions-warning.title": {
"message": "{count, plural, one {La versione {version_name} è stata sospesa} other {Alcune versioni sono state sospese}} per attribuzioni incomplete"
},
"report.already-reported": {
"message": "Hai già segnalato {title}"
},
@@ -3002,6 +3089,9 @@
"search.filter.locked.server.sync": {
"message": "Sincronizza col server"
},
"servers.manage.content.title": {
"message": "Contenuto - {serverName} - Modrinth"
},
"servers.notice.actions": {
"message": "Azioni"
},
@@ -3219,7 +3309,7 @@
"message": "Inserisci codice 2FA"
},
"settings.account.two-factor.field.code.placeholder": {
"message": "Inserisci codice..."
"message": "Inserisci il codice..."
},
"settings.account.two-factor.setup.intro": {
"message": "L'autenticazione a due fattori (2FA) protegge il tuo account richiedendo un dispositivo secondario per effettuare l'accesso."
@@ -3228,7 +3318,7 @@
"message": "Se il codice QR non funziona, inserisci manualmente questa chiave:"
},
"settings.account.two-factor.setup.scan": {
"message": "Inizia scansionando il codice QR con <authy-link>Authy</authy-link>, <microsoft-authenticator-link>Microsoft Authenticator</microsoft-authenticator-link> o qualsiasi altra app per il 2FA."
"message": "Inizia scansionando il codice QR con <authy-link>Authy</authy-link>, <microsoft-authenticator-link>Microsoft Authenticator</microsoft-authenticator-link> o qualsiasi altra app di autenticazione."
},
"settings.account.two-factor.verify.description": {
"message": "Inserisci il codice generato dall'app di autenticazione per confermare l'accesso."
+13 -1
View File
@@ -1244,6 +1244,12 @@
"error.collection.404.title": {
"message": "コレクションが見つかりません"
},
"error.generic.401.signed-in-as": {
"message": "現在サインイン中のアカウント:"
},
"error.generic.401.title": {
"message": "このページにアクセスする権限がありません"
},
"error.generic.404.subtitle": {
"message": "お探しのページは存在しないようです。"
},
@@ -1670,6 +1676,9 @@
"layout.action.create-new": {
"message": "新規作成…"
},
"layout.action.external-projects": {
"message": "外部プロジェクト"
},
"layout.action.file-lookup": {
"message": "ファイル検索"
},
@@ -1922,6 +1931,9 @@
"moderation.moderate": {
"message": "適度"
},
"moderation.page.external-projects": {
"message": "外部プロジェクト"
},
"moderation.page.projects": {
"message": "プロジェクト"
},
@@ -1929,7 +1941,7 @@
"message": "レポート"
},
"moderation.page.technicalReview": {
"message": "技術的レビュー"
"message": "技術評価"
},
"muralpay.account-type.checking": {
"message": "チェック中"
+61 -1
View File
@@ -1244,6 +1244,12 @@
"error.collection.404.title": {
"message": "컬렉션을 찾을 수 없습니다"
},
"error.generic.401.signed-in-as": {
"message": "현재 로그인된 계정:"
},
"error.generic.401.title": {
"message": "이 페이지에 접근할 권한이 없습니다"
},
"error.generic.404.subtitle": {
"message": "찾으시려는 페이지가 존재하지 않는 것 같습니다."
},
@@ -1670,6 +1676,9 @@
"layout.action.create-new": {
"message": "새로 만들기..."
},
"layout.action.external-projects": {
"message": "외부 프로젝트"
},
"layout.action.file-lookup": {
"message": "파일 찾기"
},
@@ -1922,6 +1931,9 @@
"moderation.moderate": {
"message": "관리"
},
"moderation.page.external-projects": {
"message": "외부 프로젝트"
},
"moderation.page.projects": {
"message": "프로젝트"
},
@@ -1929,7 +1941,7 @@
"message": "신고"
},
"moderation.page.technicalReview": {
"message": "기술 검토"
"message": "기술 리뷰"
},
"muralpay.account-type.checking": {
"message": "입출금"
@@ -2615,6 +2627,45 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.attention-needed.description.proj-approved": {
"message": "다음 파일들을 재배포할 권한이 있음을 증명하는 자료를 제출 바랍니다. 제출 시 보류된 버전은 자동으로 공개됩니다."
},
"project.settings.permissions.attention-needed.description.proj-draft": {
"message": "프로젝트 검토를 위해 제출하기 전에 다음 파일들을 재배포할 권한이 있음을 증명하는 자료를 제출해 주세요."
},
"project.settings.permissions.attention-needed.title": {
"message": "알 수 없는 내장 콘텐츠"
},
"project.settings.permissions.completed.description": {
"message": "모든 외부 콘텐츠에는 출처가 명시되어 있습니다."
},
"project.settings.permissions.completed.title": {
"message": "출처 표기 완료됨!"
},
"project.settings.permissions.empty-state.description": {
"message": "어떤 버전에도 외부 콘텐츠가 포함되어 있지 않으므로 권한을 얻는 걸 걱정하지 않아도 됩니다."
},
"project.settings.permissions.empty-state.heading": {
"message": "준비됨!"
},
"project.settings.permissions.fail.description": {
"message": "추가한 외부 콘텐츠 중 일부를 재배포할 권한이 없습니다. Modrinth에 게시하려면 저작권 침해 콘텐츠를 삭제해야 합니다."
},
"project.settings.permissions.fail.title": {
"message": "일부 콘텐츠는 포함될 수 없습니다"
},
"project.settings.permissions.info-banner.description": {
"message": "Modrinth에 호스팅되지 않은 콘텐츠를 포함하는 경우, 해당 콘텐츠의 출처를 알리고 파일 배포 권한이 있는지 확인해야 합니다. <link>저희 지침</link>을 참고해 어떻게 처리하는지 알아보세요!"
},
"project.settings.permissions.info-banner.title": {
"message": "출처 표기 방식 알아보기"
},
"project.settings.permissions.learn-more": {
"message": "더보기"
},
"project.settings.permissions.search-placeholder": {
"message": "{count}개의 {count, plural, other {외부 프로젝트}} 검색..."
},
"project.settings.title": {
"message": "설정"
},
@@ -2633,6 +2684,15 @@
"project.versions.title": {
"message": "버전"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, other {해당 버전은}} 현재 보류 상태이며 공개되지 않았습니다. 이 모드팩 {count, plural, other {버전에}} 포함된 특정 파일들을 재배포할 수 있는 권한이 있음을 증명하는 자료를 제출해주세요."
},
"project.versions.withheld-versions-warning.resolve-button": {
"message": "해결"
},
"project.versions.withheld-versions-warning.title": {
"message": "알 수 없는 내장 콘텐츠로 인해 일부 {count, plural, other {버전}}은 공개되지 않았습니다"
},
"report.already-reported": {
"message": "이미 {title} 을(를) 신고했습니다"
},
+27 -3
View File
@@ -848,9 +848,18 @@
"dashboard.creator-withdraw-modal.withdraw-limit-used": {
"message": "Anda telah menggunakan had pengeluaran anda sebanyak <b>{withdrawLimit}</b>. Anda mesti melengkapkan borang cukai untuk mengeluarkan lebih banyak wang."
},
"dashboard.notifications.history.label": {
"message": "Sejarah"
},
"dashboard.projects.links.description": {
"message": "Sebarang pautan yang anda nyatakan di bawah akan ditulis ganti pada setiap projek yang dipilih. Sebarang pautan yang anda biarkan kosong akan diabaikan. Anda boleh memadamkan pautan daripada semua projek yang dipilih menggunakan butang tong sampah."
},
"dashboard.projects.sort.option.name": {
"message": "Nama"
},
"dashboard.projects.sort.option.status": {
"message": "Status"
},
"dashboard.projects.sort.option.type": {
"message": "Jenis"
},
@@ -863,6 +872,9 @@
"dashboard.projects.table.name": {
"message": "Nama"
},
"dashboard.projects.table.status": {
"message": "Status"
},
"dashboard.projects.table.type": {
"message": "Jenis"
},
@@ -1022,6 +1034,15 @@
"dashboard.withdraw.error.tax-form.title": {
"message": "Sila lengkapkan borang cukai"
},
"discover.seo.description": {
"message": "Cari dan layari beribu-ribu {projectType, select, mod {mod} modpack {pek mod} resourcepack {pek sumber} shader {pembayang} plugin {pemalam} datapack {pek data} other {projek}} Minecraft di Modrinth dengan hasil carian yang segera dan tepat. Penapis kami akan membantu anda mencari {projectType, select, mod {mod} modpack {pek mod} resourcepack {pek sumber} shader {pembayang} plugin {pemalam} datapack {pek data} other {projek}} yang terbaik."
},
"discover.seo.title": {
"message": "Cari {projectType, select, mod {mod} modpack {pek mod} resourcepack {pek sumber} shader {pembayang} plugin {pemalam} datapack {pek data} other {projek}}"
},
"discover.seo.title-with-query": {
"message": "Cari {projectType, select, mod {mod} modpack {pek mod} resourcepack {pek sumber} shader {pembayang} plugin {pemalam} datapack {pek data} other {projek}} | {query}"
},
"error.collection.404.list_item.1": {
"message": "Anda mungkin tersalah taip URL koleksi."
},
@@ -1721,9 +1742,6 @@
"moderation.page.reports": {
"message": "Laporan"
},
"moderation.page.technicalReview": {
"message": "Penilaian Teknikal"
},
"muralpay.account-type.checking": {
"message": "Cek"
},
@@ -2408,6 +2426,9 @@
"project.settings.general.url.title": {
"message": "URL"
},
"project.settings.permissions.info-banner.description": {
"message": "Jika anda memasukkan kandungan yang tidak dihoskan di Modrinth, anda perlu memberitahu kami dari mana ia berasal dan mengesahkan bahawa anda mempunyai kebenaran untuk mengedarkan fail tersebut. Lihat <link>panduan kami</link> untuk mengetahui cara untuk melakukannya dengan betul!"
},
"project.settings.title": {
"message": "Tetapan"
},
@@ -2426,6 +2447,9 @@
"project.versions.title": {
"message": "Versi"
},
"project.versions.withheld-versions-warning.description": {
"message": "{count, plural, other {Versi ini}} kini ditahan dan tidak disenaraikan secara umum. Sila berikan bukti bahawa anda mempunyai kebenaran untuk mengedarkan semula fail tertentu yang disertakan dalam {count, plural, other {versi}} pek mod ini."
},
"report.already-reported": {
"message": "Anda telah melaporkan {title}"
},
@@ -1703,9 +1703,6 @@
"moderation.page.reports": {
"message": "Inlichtingen"
},
"moderation.page.technicalReview": {
"message": "Technische Beoordeling"
},
"muralpay.account-type.checking": {
"message": "Controleren"
},

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