Compare commits

...
Author SHA1 Message Date
tdgao adb49b8b29 feat: update sort and grouping dropdowns 2026-07-30 13:48:39 -07:00
tdgao 7b934b48a5 feat: update empty state 2026-07-30 13:26:34 -07:00
tdgao 00aaa42be1 fix: width of instance 2026-07-30 13:11:57 -07:00
tdgao be38a1aee9 refactor: instance card 2026-07-30 13:07:35 -07:00
tdgao 1f412c4bda update styles 2026-07-30 12:48:12 -07:00
tdgao 0a31725c8f refactor: separate out instance card content 2026-07-30 12:28:51 -07:00
tdgao b10b6b86c7 feat: update instance card styles 2026-07-30 12:23:46 -07:00
tdgao 01c023b9f4 feat: create instance with content in creation flow modal
- search modpack becomes search projects
- creation flow modal context receives prepareProjectInstall and createProjectInstall callbacks, from content-install.ts (shared with ContentInstallModal)
2026-07-30 12:04:50 -07:00
tdgao 2f618dbd1e feat: add tooltip for instance already in group 2026-07-30 10:31:33 -07:00
tdgao 7ca97c4864 pnpm prepr 2026-07-29 16:25:29 -07:00
tdgao 7a9e6c451b feat: creating groups with selected instances and fix some moving selection and duplication bugs 2026-07-29 16:21:28 -07:00
tdgao 2dfcc57b18 fix: use ButtonStyled in Chips component 2026-07-29 15:59:42 -07:00
tdgao 0fe42182bd feat: set size on microsoft login screen 2026-07-29 15:55:10 -07:00
tdgao 5102efce2a fix: show getting started checklist in all sidebar 2026-07-29 15:35:16 -07:00
tdgao 69569bf468 feat: strokes 2026-07-29 15:29:16 -07:00
tdgao 304b8338f8 feat: create group button, fix instance selection, and increase group name max length 2026-07-29 15:13:58 -07:00
tdgao 1454e5baa8 feat: creating new group always adds instance 2026-07-29 14:46:12 -07:00
tdgao 6bb69c8d52 refactor: instance groups to be identified by ID instead of name, names become display data 2026-07-29 14:43:03 -07:00
tdgao 5ad5fa829f fix: multiple context menus can be opened 2026-07-27 18:49:58 -07:00
tdgao eb86926b52 feat: update context menu 2026-07-27 18:47:15 -07:00
tdgao c943ba1f9b feat: update sorting and moved label 2026-07-27 18:44:54 -07:00
tdgao 0897a6e782 feat: add esc while dragging to cancel 2026-07-27 18:30:58 -07:00
tdgao 26c4e710f8 fix: icon disappear transition 2026-07-27 18:27:01 -07:00
tdgao 2b0f698a0c feat: add shift to select multiple instances 2026-07-27 18:24:01 -07:00
tdgao 9edbee374e feat: improve ungrouped behaviour 2026-07-27 18:10:20 -07:00
tdgao 2ea7c1ab40 feat: change open/collapse group target 2026-07-27 17:59:48 -07:00
tdgao d8c96c09a8 refactor: dont prefix filenames 2026-07-27 17:54:39 -07:00
tdgao 277615b079 feat: implement sort and filter options 2026-07-27 17:51:34 -07:00
tdgao c1b3476bd8 fix: bad routes.js 2026-07-27 17:26:00 -07:00
tdgao c0acc7e2a9 Merge branch 'main' into truman/app-onboarding 2026-07-27 17:00:52 -07:00
tdgao 5daee756c2 feat: improve icon hiding delay 2026-07-27 16:53:51 -07:00
tdgao c2fb6e168b feat: add delay on icon hiding in inline input 2026-07-27 16:44:51 -07:00
tdgao ccb40de61c feat: add remove from group in action bar 2026-07-27 16:39:43 -07:00
tdgao 57c167e5d8 small 2026-07-27 16:32:47 -07:00
tdgao 26a5a16612 feat: drag and drop multiple selected 2026-07-27 16:30:12 -07:00
tdgao 068492aba4 fix: dont toggle group when context menu open 2026-07-27 15:22:46 -07:00
tdgao 281aea7996 feat: improve selection behaviour 2026-07-27 15:09:08 -07:00
tdgao 4f2c053f7b small style fixes 2026-07-27 14:38:18 -07:00
tdgao f31477b4d7 small fix 2026-07-27 14:32:10 -07:00
tdgao b9f7bc7144 feat: improve edit group name 2026-07-27 14:30:16 -07:00
tdgao b33d442aa5 feat: show ungrouped with header 2026-07-27 14:15:29 -07:00
7c4190fb9c feat: move users page to xplat page system (#6889)
* feat: move users page to xplat

* feat: stop doing tauri plugin open for user links

* feat: qa

* fix: qa

* fix: qa

* fix: comment

* fix: prepr

* prepr

* prepr

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-27 20:02:33 +00:00
SychicandGitHub ebc7cac8b0 feat(labrinth): user blocking (#6837)
* feat(labrinth): user blocking

still wip, nothing is hooked up

* style(labrinth): cargo fmt

* refactor(labrinth): move block/unblock routes to public api

* feat(labrinth): block friend requests if blocked

* feat(labrinth): remove friend/request on block

* feat(labrinth): get blocked users endpoint
2026-07-27 19:41:57 +00:00
François-Xavier TalbotandGitHub 224c28f29d feat: more details linked account info for support/moderation (#6897)
* feat: show discord + steam ID + github profile in "Show details" user modal

* pnpm intl:extract
2026-07-27 19:20:38 +00:00
aecsocketandGitHub e192f6a0af feat: define default search vars in env (#6902)
* feat: define default search vars in env

* fix
2026-07-27 19:17:14 +00:00
tdgao 2d6bce3d38 feat: drag and drop to move between groups 2026-07-27 12:14:14 -07:00
ProspectorandGitHub c938ca3ceb update toggle filter design (#6888)
change toggle filters to just use exclude-only filters, same design as the others.
2026-07-27 11:49:52 -07:00
François-Xavier TalbotandGitHub 20adb0f16a fix(labrinth): let through emails when neverbounce hangs or fails (#6898)
* fix(labrinth): let through emails when neverbounce hangs or fails

* chore(ci): stack size
2026-07-27 18:10:54 +00:00
tdgao 9625a4221d feat: implement action bar for selection 2026-07-27 10:36:27 -07:00
Calum H.andGitHub 354940a8c6 fix: search resetting to page 1 (#6896) 2026-07-27 15:09:13 +00:00
4c2e64e31f fix: try fix install jobs being bricked/improve tracing (#6890)
* fix: try fix install jobs being bricked/improve tracing

* fix: rev cmts

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-27 14:47:46 +00:00
Modrinth BotandGitHub 372e2bf47b New translations from Crowdin (main) (#6892) 2026-07-27 10:46:42 +00:00
Magnus JensenandGitHub 36d786f504 Fix vertical mis-aligned badge in notifcations (#6887)
vertical align badge
2026-07-26 20:25:19 +00:00
Prospector 1a1e1ea448 changelog 2026-07-26 12:22:50 -07:00
ProspectorandGitHub 24240d514a update 24-48hrs -> within a week to give more realistic estimate (#6886) 2026-07-26 12:06:31 -07:00
aecsocketandGitHub 158de019a6 fix: search indexing performance and batching (#6521)
* Add consume batching delay

* maybe fix

* max batch size

* more logging

* parallelize remove tasks

* delete/upsert project/version messages

* prepare

* more logging

* log number of docs

* try more targeted, homogenous version change ops

* ensure only necessary fields are serialized into typesense

* disable index background task

* wip: script changes

* don't facet by project id

* fix

* fix clippy

* batch by document and dedup loaders

* fix test

* wip: projects/versions collections

* clean up SearchBackend interface

* cleanup pass

* cleanup pass 2

* standardise fn names

* cleanup pass

* fix compile

* factor out filter rewriting

* query perf

* put categories into the project doc

* wip: search filter AST

* doc comment

* more AST normalization

* (temp) convert search request errors into internal errors

* implement unary NOT

* tombi fmt

* fix tests

* revert request error

* try increase stack size
2026-07-26 14:55:00 +00:00
Prospector 54cc1898fa changelog 2026-07-25 18:32:56 -07:00
b96a1cfa58 fix: allow crowdin badges to skip image proxy (#6861)
Add crowdin to allowed hostnames

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-26 01:14:21 +00:00
59a794ebb1 Minor UX UI tweaks (#6596)
* feat(ux): Add letter counters to the bio and username

the username letter counter only appears after the 30th letter to not encourage people to have long usernames for no reason

* feat(ux): Added "Expand" button to bio on mobile

* Revert "feat(ux): Added "Expand" button to bio on mobile"

This reverts commit f7e036002a.

* prepr

---------

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-26 00:47:14 +00:00
b4380aee87 feat(moderation): modpack scanning improvements (#6843)
* feat(moderation): make modpack scanning improvements

* oopsie

* feat(moderation): batch size for modpack scan

* feat: i18n for modpack scan modal

* chore: remove debug line

* run intl:extract and prepr

---------

Co-authored-by: coolbot100s <76798835+coolbot100s@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-07-26 00:14:09 +00:00
François-Xavier TalbotandGitHub 2cf71a53f2 chore(ci): increase rust stack size (#6874) 2026-07-26 00:11:08 +00:00
coolbotandGitHub 44be49416b feat: update private use moderation msgs and stage (#6872) 2026-07-26 00:10:48 +00:00
ProspectorandGitHub 7f07e66e4f fix: users not seeing moderation page banners and info 🤦 (#6882) 2026-07-25 17:10:24 -07:00
tdgao e271bb6878 feat: add selected instance item state 2026-07-24 15:24:56 -07:00
tdgao de37804f54 feat: style instance items in library 2026-07-24 15:11:48 -07:00
tdgao aa7328b392 feat: add context menu for groups 2026-07-24 14:41:06 -07:00
tdgao f1b907086e refactor: move instance group api out 2026-07-24 14:35:17 -07:00
tdgao 855f57b6b3 feat: implement rename instance groups 2026-07-24 14:30:38 -07:00
tdgao 1a777fd6de feat: add delete group 2026-07-24 14:00:59 -07:00
tdgao 4b30882e4b feat: update accordian for instance groups 2026-07-24 13:26:19 -07:00
tdgao e64bc58d89 feat: add instance group definitions 2026-07-24 13:06:22 -07:00
tdgao 46710c0b3e feat: add create group modal 2026-07-24 12:40:39 -07:00
tdgao c2821d69d4 refactor: split up the current library page files 2026-07-24 11:51:43 -07:00
tdgao 143e9b4b8b refactor: rename existing components 2026-07-24 11:27:36 -07:00
tdgao e9ec976cd7 Merge branch 'main' into truman/app-onboarding 2026-07-24 11:01:18 -07:00
tdgao aea761252b refactor: move grid display into /library 2026-07-22 11:57:34 -07:00
tdgao 14db1ae49c refactor: move welcome screen to component 2026-07-22 11:35:33 -07:00
tdgao dfb23fd6ac feat: remove discover section from homepage and add library 2026-07-22 11:31:26 -07:00
tdgao 184e6fb1ab feat-small: style tweak 2026-07-22 10:31:14 -07:00
tdgao 1ec6eef0ae feat: split up import modpack button 2026-07-20 12:25:30 -07:00
tdgao ede847f84c feat: move checklist visibility source of truth in app db 2026-07-17 13:37:33 -07:00
tdgao 90de608504 feat: add onboarding checklist component 2026-07-15 11:32:40 -07:00
tdgao 7782458ec9 feat: add welcome page 2026-07-15 11:07:42 -07:00
tdgao f4086f2ec6 feat: add onboarding checklist app db and add vue provider 2026-07-15 10:33:03 -07:00
tdgao 907599a69e sqlx prepare 2026-07-15 09:53:16 -07:00
tdgao b315c8acfa refactor: remove unused legacy onboarded flag 2026-07-15 09:43:32 -07:00
322 changed files with 18768 additions and 8664 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
[target.'cfg(windows)']
[target."cfg(windows)"]
rustflags = ["-C", "link-args=/STACK:16777220"]
[target.x86_64-pc-windows-msvc]
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
REDIS_CONNECTION_TYPE: multiplexed
REDIS_URL: redis://127.0.0.1:7000,redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003,redis://127.0.0.1:7004,redis://127.0.0.1:7005
# Avoid stack overflows in tests
RUST_MIN_STACK: 16777216
RUST_MIN_STACK: 134217728
steps:
- name: Check out code
Generated
+2
View File
@@ -5465,6 +5465,7 @@ dependencies = [
"bytes",
"censor",
"chrono",
"chumsky",
"clap 4.5.48",
"clickhouse",
"color-eyre",
@@ -5518,6 +5519,7 @@ dependencies = [
"serde_with",
"sha1 0.10.6",
"sha2",
"smallvec",
"spdx",
"sqlx",
"sqlx-tracing",
+2
View File
@@ -59,6 +59,7 @@ bytes = "1.10.1"
censor = "0.3.0"
chardetng = "0.1.17"
chrono = "0.4.42"
chumsky = "0.9.3"
cidre = { version = "0.15.0", default-features = false, features = [
"macos_15_0"
] }
@@ -192,6 +193,7 @@ sha1 = "0.10.6"
sha1_smol = { version = "1.0.1", features = ["std"] }
sha2 = "0.10.9"
shlex = "1.3.0"
smallvec = "1.15.1"
spdx = "0.12.0"
sqlx = { version = "0.8.6", default-features = false }
sqlx-tracing = { path = "packages/sqlx-tracing" }
+1
View File
@@ -14,6 +14,7 @@
"test": "vue-tsc --noEmit"
},
"dependencies": {
"@dnd-kit/vue": "^0.5.0",
"@modrinth/api-client": "workspace:^",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
+73 -37
View File
@@ -13,13 +13,12 @@ import {
ChangeSkinIcon,
CompassIcon,
ExternalIcon,
HomeIcon,
LeftArrowIcon,
LibraryIcon,
LogInIcon,
LogOutIcon,
NewspaperIcon,
NotepadTextIcon,
PlayIcon,
PlusIcon,
RefreshCwIcon,
RightArrowIcon,
@@ -83,6 +82,7 @@ import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyIn
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import OnboardingChecklist from '@/components/ui/onboarding-checklist/index.vue'
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
@@ -115,6 +115,7 @@ 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 { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts'
import { parse_modrinth_user_link } from '@/helpers/users'
import {
areUpdatesEnabled,
enqueueUpdateForInstallation,
@@ -265,7 +266,7 @@ providePageContext({
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
),
},
openExternalUrl: (url) => openUrl(url),
openExternalUrl: (url) => void openUrl(url),
})
provideModalBehavior({
noblur: computed(() => !themeStore.advancedRendering),
@@ -279,13 +280,15 @@ const {
fetchExistingInstanceNames,
handleCreate,
handleBrowseModpacks,
searchModpacks,
getProjectVersions,
searchProjects,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
onboardingChecklist,
} = setupProviders(notificationManager, popupNotificationManager)
const { hasLoggedIntoMinecraft, hasLoggedIntoModrinth, showChecklist } = onboardingChecklist
const showFriendsList = computed(() => !showChecklist.value || hasLoggedIntoModrinth.value)
const news = ref([])
const availableSurvey = ref(false)
@@ -302,7 +305,6 @@ window.addEventListener('online', () => {
offline.value = false
})
const showOnboarding = ref(false)
const nativeDecorations = ref(false)
const os = ref('')
@@ -453,6 +455,8 @@ function handleAdsConsentRequired(required) {
}
async function setupApp() {
await onboardingChecklist.initialize()
const {
native_decorations,
theme,
@@ -461,8 +465,6 @@ async function setupApp() {
collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
onboarded,
default_page,
toggle_sidebar,
developer_mode,
feature_flags,
@@ -474,16 +476,10 @@ async function setupApp() {
i18n.global.locale.value = locale
}
if (default_page === 'Library') {
await router.push('/library')
}
os.value = await getOS()
const dev = await isDev()
isDevEnvironment.value = dev
const version = await getVersion()
showOnboarding.value = !onboarded
nativeDecorations.value = native_decorations
if (os.value !== 'MacOS') await getCurrentWindow().setDecorations(native_decorations)
@@ -505,7 +501,7 @@ async function setupApp() {
if (telemetry) {
initAnalytics()
if (dev) debugAnalytics()
trackEvent('Launched', { version, dev, onboarded })
trackEvent('Launched', { version, dev })
}
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
@@ -722,6 +718,7 @@ const {
projectInfo: contentInstallProjectInfo,
handleInstallToInstance,
handleCreateAndInstall,
prepareNewInstance,
handleNavigate: handleContentInstallNavigate,
handleCancel: handleContentInstallCancel,
setContentInstallModal,
@@ -741,6 +738,35 @@ const {
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
async function prepareCreationProjectInstall(projectId, projectType) {
if (projectType === 'modpack') {
await contentInstall.install(
projectId,
null,
null,
'CreationModalProject',
undefined,
(instanceId) => void router.push(`/instance/${encodeURIComponent(instanceId)}`),
)
return null
}
await prepareNewInstance(projectId)
const info = contentInstallProjectInfo.value
if (!info) throw new Error(`Project information is unavailable: '${projectId}'`)
return {
projectId,
title: info.title,
iconUrl: info.iconUrl,
link: info.link,
owner: info.owner,
compatibleLoaders: [...contentInstallLoaders.value],
gameVersions: [...contentInstallGameVersions.value],
releaseGameVersions: new Set(contentInstallReleaseGameVersions.value),
}
}
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
provideServerInstall(serverInstall)
const {
@@ -966,7 +992,7 @@ async function declineServerInviteNotification(notification) {
function openServerInviteInviterProfile(inviterName) {
if (!inviterName) return
openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`)
void router.push(`/user/${encodeURIComponent(inviterName)}`)
}
async function handleLiveNotification(notification) {
@@ -1407,8 +1433,11 @@ function handleClick(e) {
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
) {
const userPath = parse_modrinth_user_link(target.href)
const parsed = parseModrinthLink(target.href)
if (target.target !== '_blank' && parsed) {
if (userPath) {
void router.push(userPath)
} else if (target.target !== '_blank' && parsed) {
void openModrinthProjectLinkInApp(parsed)
} else {
openUrl(target.href)
@@ -1581,8 +1610,9 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
type="instance"
show-snapshot-toggle
:fetch-existing-instance-names="fetchExistingInstanceNames"
:search-modpacks="searchModpacks"
:get-project-versions="getProjectVersions"
:search-projects="searchProjects"
:prepare-project-install="prepareCreationProjectInstall"
:create-project-install="handleCreateAndInstall"
:get-loader-manifest="getLoaderManifest"
@create="handleCreate"
@browse-modpacks="handleBrowseModpacks"
@@ -1591,8 +1621,16 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<div
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
>
<NavButton v-tooltip.right="'Home'" to="/">
<HomeIcon />
<NavButton
v-tooltip.right="'Home'"
to="/"
:is-primary="(route) => route.path === '/'"
:is-subpage="
() =>
(route.path.startsWith('/browse') || route.path.startsWith('/project')) && route.query.i
"
>
<PlayIcon />
</NavButton>
<NavButton v-if="themeStore.featureFlags.worlds_tab" v-tooltip.right="'Worlds'" to="/worlds">
<WorldIcon />
@@ -1608,19 +1646,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<NavButton v-tooltip.right="'Skin selector'" to="/skins">
<ChangeSkinIcon />
</NavButton>
<NavButton
v-tooltip.right="'Library'"
to="/library"
:is-primary="(r) => r.path === '/library' || r.path === '/library'"
:is-subpage="
() =>
route.path.startsWith('/instance') ||
((route.path.startsWith('/browse') || route.path.startsWith('/project')) &&
route.query.i)
"
>
<LibraryIcon />
</NavButton>
<NavButton
v-tooltip.right="'Modrinth Hosting'"
to="/hosting/manage"
@@ -1654,7 +1679,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
:options="[
{
id: 'view-profile',
action: () => openUrl('https://modrinth.com/user/' + credentials.user.username),
action: () => router.push(`/user/${encodeURIComponent(credentials.user.username)}`),
},
{
id: 'sign-out',
@@ -1819,15 +1844,26 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
:class="{ 'pb-12': !hasPlus }"
data-overlayscrollbars-initialize
>
<OnboardingChecklist
@create-instance="installationModal?.show()"
@login-minecraft="accounts?.login()"
@login-modrinth="signIn"
/>
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
<div class="sidebar-default-content" :class="{ 'sidebar-enabled': sidebarVisible }">
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
<div
v-show="hasLoggedIntoMinecraft"
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
>
<h3 class="text-base text-primary font-medium m-0">Playing as</h3>
<suspense>
<AccountsCard ref="accounts" />
</suspense>
</div>
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
<div
v-show="showFriendsList"
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
>
<suspense>
<FriendsList :credentials="credentials" :sign-in="() => requestSignIn()" />
</suspense>
@@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="30.8333" height="20.8333" viewBox="0 0 30.8333 20.8333" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M1.6228 20.8333C2.51907 20.8333 3.24562 20.1158 3.24562 19.2308L3.24563 17.5253C3.24562 17.0803 3.2459 16.016 3.7136 15.2292C4.00182 14.7442 4.37085 14.339 4.74993 14.1326C5.36078 13.8001 6.0611 13.6111 6.80555 13.6111C7.05077 13.6111 7.3144 13.6219 7.6104 13.6356L7.7201 13.6408C7.97828 13.6529 8.25927 13.6661 8.5395 13.6719C9.17298 13.6851 9.91222 13.6651 10.6258 13.4739C12.0158 13.1014 13.1015 12.0158 13.4738 10.6258C13.665 9.91222 13.685 9.17298 13.672 8.5395C13.6662 8.25937 13.6528 7.9782 13.6408 7.7201L13.6357 7.6104C13.6218 7.3144 13.6112 7.05077 13.6112 6.80555C13.6112 6.02607 13.8182 5.29497 14.1805 4.66428C14.3917 4.29663 14.7595 3.96015 15.2675 3.66463C16.0377 3.21645 16.96 3.2123 17.5237 3.20975C18.0872 3.20722 18.6818 3.20527 19.1787 3.20527C22.2357 3.20527 23.347 3.2282 24.1857 3.5193C25.6395 4.02402 26.7495 5.07765 27.2667 6.36838C27.3963 6.69185 27.4878 7.1167 27.5368 7.85255C27.5868 8.60265 27.5877 9.56012 27.5877 10.9399V19.2308C27.5877 20.1158 28.3143 20.8333 29.2105 20.8333C30.1068 20.8333 30.8333 20.1158 30.8333 19.2308V10.8854C30.8333 9.5729 30.8333 8.50977 30.7755 7.6419C30.7158 6.74778 30.5898 5.95072 30.2847 5.18902C29.4037 2.99072 27.5568 1.29222 25.262 0.495583C23.8303 -0.0014166 22.1092 -0.000816752 19.5178 8.32478e-05L17.939 0.000200073L17.9167 0L17.8582 0.000249942C13.6775 0.00308328 11.0862 0.0528001 8.93633 0.79915C5.23583 2.08378 2.2801 4.81432 0.87485 8.32102C0.40655 9.48963 0.199033 10.7449 0.0986334 12.2503C-1.66334e-05 13.7293 0 15.5563 0 17.8857V19.2308C0 20.1158 0.72655 20.8333 1.6228 20.8333Z" fill="#B0BAC5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,360 +0,0 @@
<script setup>
import {
ClipboardCopyIcon,
EyeIcon,
FolderOpenIcon,
PlayIcon,
PlusIcon,
SearchIcon,
StopCircleIcon,
TrashIcon,
} from '@modrinth/assets'
import {
Accordion,
DropdownSelect,
formatLoader,
injectNotificationManager,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import Instance from '@/components/ui/Instance.vue'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { install_duplicate_instance } from '@/helpers/install'
import { remove } from '@/helpers/instance'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const props = defineProps({
instances: {
type: Array,
default() {
return []
},
},
label: {
type: String,
default: '',
},
})
const instanceOptions = ref(null)
const instanceComponents = ref(null)
const currentDeleteInstance = ref(null)
const confirmModal = ref(null)
async function deleteInstance() {
if (currentDeleteInstance.value) {
instanceComponents.value = instanceComponents.value.filter(
(x) => x.instance.id !== currentDeleteInstance.value,
)
await remove(currentDeleteInstance.value).catch(handleError)
}
}
async function duplicateInstance(p) {
await install_duplicate_instance(p).catch(handleError)
}
const handleRightClick = (event, instanceId) => {
const item = instanceComponents.value.find((x) => x.instance.id === instanceId)
const baseOptions = [
...(item.instance.quarantined ? [] : [{ name: 'add_content' }, { type: 'divider' }]),
{ name: 'edit' },
{ name: 'duplicate' },
{ name: 'open' },
{ name: 'copy' },
{ type: 'divider' },
{
name: 'delete',
color: 'danger',
},
]
instanceOptions.value.showMenu(
event,
item,
item.playing
? [
{
name: 'stop',
color: 'danger',
},
...baseOptions,
]
: [
...(item.instance.quarantined
? []
: [
{
name: 'play',
color: 'primary',
},
]),
...baseOptions,
],
)
}
const handleOptionsClick = async (args) => {
switch (args.option) {
case 'play':
args.item.play(null, 'InstanceGridContextMenu')
break
case 'stop':
args.item.stop(null, 'InstanceGridContextMenu')
break
case 'add_content':
await args.item.addContent()
break
case 'edit':
await args.item.seeInstance()
break
case 'duplicate':
if (args.item.instance.install_stage == 'installed')
await duplicateInstance(args.item.instance.id)
break
case 'open':
await args.item.openFolder()
break
case 'copy':
await navigator.clipboard.writeText(args.item.instance.id)
break
case 'delete':
currentDeleteInstance.value = args.item.instance.id
confirmModal.value.show()
break
}
}
const state = useStorage(
`${props.label}-grid-display-state`,
{
group: 'Group',
sortBy: 'Name',
collapsedGroups: [],
},
localStorage,
{ mergeDefaults: true },
)
const search = ref('')
const collapsedSectionKeys = computed(() => new Set(state.value.collapsedGroups ?? []))
const getSectionKey = (sectionName) => `${state.value.group}:${sectionName}`
const isSectionCollapsed = (sectionName) => {
return collapsedSectionKeys.value.has(getSectionKey(sectionName))
}
const setSectionCollapsed = (sectionName, collapsed) => {
const sectionKey = getSectionKey(sectionName)
const collapsedSections = new Set(state.value.collapsedGroups ?? [])
if (collapsed) {
collapsedSections.add(sectionKey)
} else {
collapsedSections.delete(sectionKey)
}
state.value.collapsedGroups = [...collapsedSections]
}
const filteredResults = computed(() => {
const { group = 'Group', sortBy = 'Name' } = state.value
const instances = props.instances.filter((instance) => {
return instance.name.toLowerCase().includes(search.value.toLowerCase())
})
if (sortBy === 'Name') {
instances.sort((a, b) => {
return a.name.localeCompare(b.name)
})
}
if (sortBy === 'Game version') {
instances.sort((a, b) => {
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
})
}
if (sortBy === 'Last played') {
instances.sort((a, b) => {
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
})
}
if (sortBy === 'Date created') {
instances.sort((a, b) => {
return dayjs(b.date_created).diff(dayjs(a.date_created))
})
}
if (sortBy === 'Date modified') {
instances.sort((a, b) => {
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
})
}
const instanceMap = new Map()
if (group === 'Loader') {
instances.forEach((instance) => {
const loader = formatLoader(formatMessage, instance.loader)
if (!instanceMap.has(loader)) {
instanceMap.set(loader, [])
}
instanceMap.get(loader).push(instance)
})
} else if (group === 'Game version') {
instances.forEach((instance) => {
if (!instanceMap.has(instance.game_version)) {
instanceMap.set(instance.game_version, [])
}
instanceMap.get(instance.game_version).push(instance)
})
} else if (group === 'Group') {
instances.forEach((instance) => {
if (instance.groups.length === 0) {
instance.groups.push('None')
}
for (const category of instance.groups) {
if (!instanceMap.has(category)) {
instanceMap.set(category, [])
}
instanceMap.get(category).push(instance)
}
})
} else {
return instanceMap.set('None', instances)
}
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
if (sortBy === 'Name') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
// None should always be first
if (a[0] === 'None' && b[0] !== 'None') {
return -1
}
if (a[0] !== 'None' && b[0] === 'None') {
return 1
}
return a[0].localeCompare(b[0])
})
instanceMap.clear()
sortedEntries.forEach((entry) => {
instanceMap.set(entry[0], entry[1])
})
}
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
if (group === 'Game version') {
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
return a[0].localeCompare(b[0], undefined, { numeric: true })
})
instanceMap.clear()
sortedEntries.forEach((entry) => {
instanceMap.set(entry[0], entry[1])
})
}
return instanceMap
})
</script>
<template>
<div class="flex gap-2">
<StyledInput
v-model="search"
:icon="SearchIcon"
type="text"
placeholder="Search"
clearable
wrapper-class="flex-1"
/>
<DropdownSelect
v-slot="{ selected }"
v-model="state.sortBy"
name="Sort Dropdown"
class="max-w-[16rem]"
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
placeholder="Select..."
>
<span class="font-semibold text-primary">Sort by: </span>
<span class="font-semibold text-secondary">{{ selected }}</span>
</DropdownSelect>
<DropdownSelect
v-slot="{ selected }"
v-model="state.group"
class="max-w-[16rem]"
name="Group Dropdown"
:options="['Group', 'Loader', 'Game version', 'None']"
placeholder="Select..."
>
<span class="font-semibold text-primary">Group by: </span>
<span class="font-semibold text-secondary">{{ selected }}</span>
</DropdownSelect>
</div>
<Accordion
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
key,
value,
}))"
:key="instanceSection.key"
:divider="instanceSection.key !== 'None'"
:open-by-default="!isSectionCollapsed(instanceSection.key)"
class="row"
@on-open="setSectionCollapsed(instanceSection.key, false)"
@on-close="setSectionCollapsed(instanceSection.key, true)"
>
<template v-if="instanceSection.key !== 'None'" #title>
<span class="text-base">{{ instanceSection.key }}</span>
</template>
<section class="instances">
<Instance
v-for="instance in instanceSection.value"
ref="instanceComponents"
:key="instance.id + instance.install_stage"
:instance="instance"
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.id)"
/>
</section>
</Accordion>
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteInstance" />
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
<template #play> <PlayIcon /> Play </template>
<template #stop> <StopCircleIcon /> Stop </template>
<template #add_content> <PlusIcon /> Add content </template>
<template #edit> <EyeIcon /> View instance </template>
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
<template #delete> <TrashIcon /> Delete </template>
<template #open> <FolderOpenIcon /> Open folder </template>
<template #copy> <ClipboardCopyIcon /> Copy path </template>
</ContextMenu>
</template>
<style lang="scss" scoped>
.row {
width: 100%;
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
width: 100%;
gap: 0.75rem;
margin-right: auto;
scroll-behavior: smooth;
overflow-y: auto;
}
</style>
@@ -1,371 +0,0 @@
<script setup>
import {
ClipboardCopyIcon,
DownloadIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
GlobeIcon,
PlayIcon,
PlusIcon,
StopCircleIcon,
TrashIcon,
} from '@modrinth/assets'
import { HeadingLink, injectNotificationManager } from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import Instance from '@/components/ui/Instance.vue'
import LegacyProjectCard from '@/components/ui/LegacyProjectCard.vue'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { install_duplicate_instance } from '@/helpers/install'
import { kill, remove, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process.js'
import { showInstanceInFolder } from '@/helpers/utils.js'
import { injectContentInstall } from '@/providers/content-install'
import { handleSevereError } from '@/store/error.js'
const { handleError } = injectNotificationManager()
const { install: installVersion } = injectContentInstall()
const router = useRouter()
const props = defineProps({
instances: {
type: Array,
default() {
return []
},
},
label: {
type: String,
default: '',
},
canPaginate: Boolean,
})
const actualInstances = computed(() =>
props.instances.filter(
(x) => (x && x.instances && x.instances[0] && x.show === undefined) || x.show,
),
)
const modsRow = ref(null)
const instanceOptions = ref(null)
const instanceComponents = ref(null)
const rows = ref(null)
const deleteConfirmModal = ref(null)
const currentDeleteInstance = ref(null)
async function deleteInstance() {
if (currentDeleteInstance.value) {
await remove(currentDeleteInstance.value).catch(handleError)
}
}
async function duplicateInstance(p) {
await install_duplicate_instance(p).catch(handleError)
}
const handleInstanceRightClick = async (event, passedInstance) => {
const baseOptions = [
...(passedInstance.quarantined ? [] : [{ name: 'add_content' }, { type: 'divider' }]),
{ name: 'edit' },
{ name: 'duplicate' },
{ name: 'open_folder' },
{ name: 'copy_path' },
{ type: 'divider' },
{
name: 'delete',
color: 'danger',
},
]
const runningProcesses = await get_by_instance_id(passedInstance.id).catch(handleError)
const options =
runningProcesses.length > 0
? [
{
name: 'stop',
color: 'danger',
},
...baseOptions,
]
: [
...(passedInstance.quarantined
? []
: [
{
name: 'play',
color: 'primary',
},
]),
...baseOptions,
]
instanceOptions.value.showMenu(event, passedInstance, options)
}
const handleProjectClick = (event, passedInstance) => {
instanceOptions.value.showMenu(event, passedInstance, [
{
name: 'install',
color: 'primary',
},
{ type: 'divider' },
{
name: 'open_link',
},
{
name: 'copy_link',
},
])
}
const handleOptionsClick = async (args) => {
switch (args.option) {
case 'play':
await run(args.item.id).catch((err) => handleSevereError(err, { instanceId: args.item.id }))
trackEvent('InstanceStart', {
loader: args.item.loader,
game_version: args.item.game_version,
})
break
case 'stop':
await kill(args.item.id).catch(handleError)
trackEvent('InstanceStop', {
loader: args.item.loader,
game_version: args.item.game_version,
})
break
case 'add_content':
await router.push({
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: args.item.id },
})
break
case 'edit':
await router.push({
path: `/instance/${encodeURIComponent(args.item.id)}`,
})
break
case 'duplicate':
if (args.item.install_stage == 'installed') await duplicateInstance(args.item.id)
break
case 'delete':
currentDeleteInstance.value = args.item.id
deleteConfirmModal.value.show()
break
case 'open_folder':
await showInstanceInFolder(args.item.id)
break
case 'copy_path':
await navigator.clipboard.writeText(args.item.id)
break
case 'install': {
await installVersion(
args.item.project_id,
null,
null,
'ProjectCardContextMenu',
() => {},
() => {},
).catch(handleError)
break
}
case 'open_link':
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
break
case 'copy_link':
await navigator.clipboard.writeText(
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
)
break
}
}
const maxInstancesPerCompactRow = ref(1)
const maxInstancesPerRow = ref(1)
const maxProjectsPerRow = ref(1)
const calculateCardsPerRow = () => {
if (rows.value.length === 0) {
return
}
// Calculate how many cards fit in one row
const containerWidth = rows.value[0].clientWidth
// Convert container width from pixels to rem
const containerWidthInRem =
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
maxInstancesPerCompactRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 0.75) / 20.75)
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 0.75) / 18.75)
if (maxInstancesPerRow.value < 5) {
maxInstancesPerRow.value *= 2
}
if (maxInstancesPerCompactRow.value < 5) {
maxInstancesPerCompactRow.value *= 2
}
if (maxProjectsPerRow.value < 3) {
maxProjectsPerRow.value *= 2
}
}
const rowContainer = ref(null)
const resizeObserver = ref(null)
onMounted(() => {
calculateCardsPerRow()
resizeObserver.value = new ResizeObserver(calculateCardsPerRow)
if (rowContainer.value) {
resizeObserver.value.observe(rowContainer.value)
}
window.addEventListener('resize', calculateCardsPerRow)
})
onUnmounted(() => {
window.removeEventListener('resize', calculateCardsPerRow)
if (rowContainer.value) {
resizeObserver.value.unobserve(rowContainer.value)
}
})
</script>
<template>
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="deleteInstance" />
<div ref="rowContainer" class="flex flex-col gap-4">
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
<HeadingLink class="mt-1" :to="row.route">
{{ row.label }}
</HeadingLink>
<section
v-if="row.instance"
ref="modsRow"
class="instances"
:class="{ compact: row.compact }"
>
<Instance
v-for="(instance, instanceIndex) in row.instances.slice(
0,
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
)"
:key="row.label + instance.id"
:instance="instance"
:compact="row.compact"
:first="instanceIndex === 0"
@contextmenu.prevent.stop="(event) => handleInstanceRightClick(event, instance)"
/>
</section>
<section v-else ref="modsRow" class="projects">
<LegacyProjectCard
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
:key="project?.project_id"
ref="instanceComponents"
class="item"
:project="project"
@contextmenu.prevent.stop="(event) => handleProjectClick(event, project)"
/>
</section>
</div>
</div>
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
<template #play> <PlayIcon /> Play </template>
<template #stop> <StopCircleIcon /> Stop </template>
<template #add_content> <PlusIcon /> Add content </template>
<template #edit> <EyeIcon /> View instance </template>
<template #delete> <TrashIcon /> Delete </template>
<template #open_folder> <FolderOpenIcon /> Open folder </template>
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
<template #copy_path> <ClipboardCopyIcon /> Copy path </template>
<template #install> <DownloadIcon /> Install </template>
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
</ContextMenu>
</template>
<style lang="scss" scoped>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
gap: 1rem;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
background: transparent;
}
}
.row {
display: flex;
flex-direction: column;
align-items: flex-start;
overflow: hidden;
width: 100%;
min-width: 100%;
&:nth-child(even) {
background: var(--color-bg);
}
.header {
width: 100%;
margin-bottom: 1rem;
gap: var(--gap-xs);
display: flex;
flex-direction: row;
align-items: center;
a {
margin: 0;
font-size: var(--font-size-md);
font-weight: bolder;
white-space: nowrap;
color: var(--color-base);
}
svg {
height: 1.25rem;
width: 1.25rem;
color: var(--color-base);
}
}
.instances {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
grid-gap: 0.75rem;
width: 100%;
&.compact {
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
gap: 0.75rem;
}
}
.projects {
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
grid-gap: 0.75rem;
.item {
width: 100%;
max-width: 100%;
}
}
}
</style>
@@ -182,6 +182,7 @@ defineExpose({
refreshValues,
setEquippedSkin,
setLoginDisabled,
login,
loginDisabled,
})
@@ -34,9 +34,18 @@ const options = ref([])
const left = ref('0px')
const top = ref('0px')
const shown = ref(false)
const contextMenuId = Symbol()
const contextMenuOpenEvent = 'modrinth-context-menu-open'
const hideContextMenu = () => {
shown.value = false
emit('menu-closed')
}
defineExpose({
showMenu: (event, passedItem, passedOptions) => {
window.dispatchEvent(new CustomEvent(contextMenuOpenEvent, { detail: contextMenuId }))
item.value = passedItem
options.value = passedOptions
@@ -62,6 +71,7 @@ defineExpose({
}
})
},
hideMenu: hideContextMenu,
})
const isInstanceLink = (item) => {
@@ -73,11 +83,6 @@ const isInstanceLink = (item) => {
return false
}
const hideContextMenu = () => {
shown.value = false
emit('menu-closed')
}
const optionClicked = (option) => {
emit('option-clicked', {
item: item.value,
@@ -92,6 +97,12 @@ const onEscKeyRelease = (event) => {
}
}
const handleContextMenuOpen = (event) => {
if (shown.value && event.detail !== contextMenuId) {
hideContextMenu()
}
}
const handleClickOutside = (event) => {
const elements = document.elementsFromPoint(event.clientX, event.clientY)
if (
@@ -105,11 +116,13 @@ const handleClickOutside = (event) => {
onMounted(() => {
window.addEventListener('click', handleClickOutside)
window.addEventListener(contextMenuOpenEvent, handleContextMenuOpen)
document.body.addEventListener('keyup', onEscKeyRelease)
})
onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutside)
window.removeEventListener(contextMenuOpenEvent, handleContextMenuOpen)
document.removeEventListener('keyup', onEscKeyRelease)
})
</script>
@@ -1,278 +0,0 @@
<script setup>
import {
DownloadIcon,
GameIcon,
PlayIcon,
SpinnerIcon,
StopCircleIcon,
TimerIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import { showInstanceInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
const { handleError } = injectNotificationManager()
const formatRelativeTime = useRelativeTime()
const props = defineProps({
instance: {
type: Object,
default() {
return {}
},
},
compact: {
type: Boolean,
default: false,
},
first: {
type: Boolean,
default: false,
},
})
const playing = ref(false)
const loading = ref(false)
const modLoading = computed(
() =>
loading.value ||
currentEvent.value === 'installing' ||
(currentEvent.value === 'launched' && !playing.value),
)
const installing = computed(() => props.instance.install_stage.includes('installing'))
const installed = computed(() => props.instance.install_stage === 'installed')
const router = useRouter()
const seeInstance = async () => {
await router.push(`/instance/${encodeURIComponent(props.instance.id)}`)
}
const checkProcess = async () => {
const runningProcesses = await get_by_instance_id(props.instance.id).catch(handleError)
playing.value = runningProcesses.length > 0
}
const play = async (e, context) => {
e?.stopPropagation()
if (props.instance.quarantined) return
loading.value = true
await run(props.instance.id)
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
.finally(() => {
trackEvent('InstanceStart', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
})
loading.value = false
}
const stop = async (e, context) => {
e?.stopPropagation()
playing.value = false
await kill(props.instance.id).catch(handleError)
trackEvent('InstanceStop', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
}
const repair = async (e) => {
e?.stopPropagation()
if (props.instance.quarantined) return
if (
props.instance.install_stage !== 'pack_installed' &&
(props.instance.link?.type === 'modrinth_modpack' ||
props.instance.link?.type === 'server_project_modpack')
) {
await install_pack_to_existing_instance(props.instance.id, {
type: 'fromVersionId',
project_id: props.instance.link.project_id ?? props.instance.link.server_project_id ?? '',
version_id: props.instance.link.version_id ?? props.instance.link.content_version_id ?? '',
title: props.instance.name,
}).catch(handleError)
} else {
await install_existing_instance(props.instance.id, false).catch(handleError)
}
}
const openFolder = async () => {
await showInstanceInFolder(props.instance.id)
}
const addContent = async () => {
if (props.instance.quarantined) return
await router.push({
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.id },
})
}
defineExpose({
play,
stop,
seeInstance,
openFolder,
addContent,
instance: props.instance,
})
const currentEvent = ref(null)
const unlisten = await process_listener((e) => {
if (e.instance_id === props.instance.id) {
currentEvent.value = e.event
if (e.event === 'finished') {
playing.value = false
}
}
})
onMounted(() => {
checkProcess()
})
onUnmounted(() => unlisten())
</script>
<template>
<template v-if="compact">
<div
class="card-shadow grid grid-cols-[auto_1fr_auto] bg-bg-raised rounded-xl p-3 pl-4 gap-2 cursor-pointer hover:brightness-90 transition-all"
@click="seeInstance"
@mouseenter="checkProcess"
>
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.id"
alt="Mod card"
/>
<div class="h-full flex items-center font-bold text-contrast leading-normal">
<span class="line-clamp-2">{{ instance.name }}</span>
</div>
<div class="flex items-center">
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
<StopCircleIcon />
</button>
</ButtonStyled>
<ButtonStyled v-else-if="modLoading" color="standard" circular>
<button v-tooltip="'Instance is loading...'" disabled>
<SpinnerIcon class="animate-spin" />
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="!instance.quarantined"
:color="first ? 'brand' : 'standard'"
circular
>
<button
v-tooltip="'Play'"
@click="(e) => play(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<!-- Translate for optical centering -->
<PlayIcon class="translate-x-[1px]" />
</button>
</ButtonStyled>
</div>
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
<TimerIcon />
<span class="text-sm">
<template v-if="instance.last_played">
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
</template>
<template v-else> Never played </template>
</span>
</div>
</div>
</template>
<div v-else>
<div
class="button-base bg-bg-raised p-4 rounded-xl flex gap-3 group"
@click="seeInstance"
@mouseenter="checkProcess"
>
<div class="relative flex items-center justify-center">
<Avatar
size="48px"
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
:tint-by="instance.id"
alt="Mod card"
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
/>
<div class="absolute inset-0 flex items-center justify-center">
<ButtonStyled v-if="playing" size="large" color="red" circular>
<button
v-tooltip="'Stop'"
:class="{ 'scale-100 opacity-100': playing }"
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
@click="(e) => stop(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<StopCircleIcon />
</button>
</ButtonStyled>
<SpinnerIcon
v-else-if="modLoading || installing"
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
class="animate-spin w-8 h-8"
tabindex="-1"
/>
<ButtonStyled
v-else-if="!installed && !instance.quarantined"
size="large"
color="brand"
circular
>
<button
v-tooltip="'Repair'"
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
@click="(e) => repair(e)"
>
<DownloadIcon />
</button>
</ButtonStyled>
<ButtonStyled v-else-if="!instance.quarantined" size="large" color="brand" circular>
<button
v-tooltip="'Play'"
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
@click="(e) => play(e, 'InstanceCard')"
@mousehover="checkProcess"
>
<PlayIcon class="translate-x-[2px]" />
</button>
</ButtonStyled>
</div>
</div>
<div class="flex flex-col gap-1">
<p class="m-0 text-md font-bold text-contrast leading-tight line-clamp-1">
{{ instance.name }}
</p>
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
<GameIcon class="shrink-0" />
<span class="text-sm capitalize">
{{ instance.loader }} {{ instance.game_version }}
</span>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { DownloadIcon, PlusIcon } from '@modrinth/assets'
import { ButtonStyled } from '@modrinth/ui'
import { inject, onMounted, onUnmounted, ref } from 'vue'
import appIcon from '../../../../app/icons/128x128.png'
const showCreationModal = inject<() => void>('showCreationModal')
const showImportModal = inject<() => void>('showImportModal')
const offline = ref(!navigator.onLine)
function handleOffline() {
offline.value = true
}
function handleOnline() {
offline.value = false
}
function handleQuickCreate(event: KeyboardEvent) {
const target = event.target as HTMLElement | null
if (
event.key.toLowerCase() !== 'n' ||
event.repeat ||
event.metaKey ||
event.ctrlKey ||
event.altKey ||
target?.isContentEditable ||
['INPUT', 'TEXTAREA', 'SELECT'].includes(target?.tagName ?? '')
) {
return
}
if (!offline.value) {
event.preventDefault()
showCreationModal?.()
}
}
onMounted(() => {
window.addEventListener('offline', handleOffline)
window.addEventListener('online', handleOnline)
window.addEventListener('keydown', handleQuickCreate)
})
onUnmounted(() => {
window.removeEventListener('offline', handleOffline)
window.removeEventListener('online', handleOnline)
window.removeEventListener('keydown', handleQuickCreate)
})
</script>
<template>
<div class="grid min-h-full grid-rows-[minmax(30.8125rem,1fr)_auto] px-6 pb-6 pt-16">
<div class="relative flex items-center justify-center">
<div
class="pointer-events-none absolute left-1/2 top-0 h-[29.875rem] w-[min(25.9375rem,80vw)] -translate-x-1/2 rounded-2xl bg-[radial-gradient(circle,var(--color-surface-5)_0.5px,transparent_0.75px)] opacity-60 [background-size:0.5625rem_0.5625rem] [mask-image:radial-gradient(ellipse_at_center,black_10%,transparent_68%)] [@media(max-height:700px)]:h-[23rem]"
aria-hidden="true"
/>
<div class="relative flex flex-col items-center gap-6">
<div
class="flex h-[6.25rem] w-[6.25rem] items-center justify-center overflow-hidden rounded-3xl bg-[linear-gradient(180deg,rgb(37_114_79_/_50%)_0%,rgb(15_64_36_/_50%)_100%),linear-gradient(-14deg,rgb(0_0_0_/_37%)_8%,transparent_86%),black] shadow-[0_0_0_1.5px_#07180d,0_1.5rem_3rem_rgb(0_0_0_/_3%),0_0.625rem_1.125rem_rgb(0_0_0_/_3%),0_0.3125rem_0.5rem_rgb(0_0_0_/_4%),0_0.125rem_0.25rem_rgb(0_0_0_/_4%),0_0.625rem_13.75rem_rgb(27_217_106_/_25%)]"
>
<img :src="appIcon" alt="" class="h-[6rem] w-[6rem]" />
</div>
<div class="flex flex-col items-center gap-2">
<h1 class="m-0 flex items-center gap-2 text-2xl font-semibold leading-8 text-contrast">
Welcome to Modrinth
</h1>
<p class="m-0 text-center text-base leading-6 text-primary">
Minecraft was never meant to fit in one jar.
</p>
</div>
<div class="flex w-72 flex-col items-center gap-4">
<ButtonStyled color="brand" size="large">
<button class="!shadow-none" :disabled="offline" @click="showCreationModal?.()">
<PlusIcon />
Create an instance
</button>
</ButtonStyled>
<span class="flex items-center gap-1 text-sm leading-5 text-secondary">
Press
<kbd
class="inline-flex h-5 min-w-5 items-center justify-center rounded-md border border-solid border-surface-5 bg-button-bg px-1 text-xs font-normal leading-4 text-primary"
>N</kbd
>
to quick create an instance
</span>
</div>
</div>
</div>
<div class="flex flex-col items-center justify-center gap-4 text-sm leading-5 text-secondary">
<span class="whitespace-nowrap">Escaping another launcher?</span>
<ButtonStyled>
<button class="!shadow-none" :disabled="offline" @click="showImportModal?.()">
<DownloadIcon />
Import from launcher
</button>
</ButtonStyled>
</div>
</div>
</template>
@@ -8,13 +8,14 @@ import {
OverflowMenu,
useVIntl,
} from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useTemplateRef } from 'vue'
import { useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import type { FriendWithUserData } from '@/helpers/friends.ts'
const { formatMessage } = useVIntl()
const router = useRouter()
const props = withDefaults(
defineProps<{
@@ -54,7 +55,7 @@ function createContextMenuOptions(friend: FriendWithUserData) {
}
function openProfile(username: string) {
openUrl('https://modrinth.com/user/' + username)
void router.push(`/user/${encodeURIComponent(username)}`)
}
const friendOptions = useTemplateRef('friendOptions')
@@ -186,8 +186,8 @@ import {
type TeleportOverflowMenuItem,
useVIntl,
} from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import type { GameInstance } from '@/helpers/types'
@@ -247,6 +247,7 @@ const messages = defineMessages({
defaultMessage: "This instance's content is being shared to other users.",
},
})
const router = useRouter()
const props = withDefaults(
defineProps<{
@@ -331,7 +332,7 @@ const sharedInstanceManagerLabel = computed(() =>
const sharedInstanceManagerAction = computed(() => {
const manager = props.sharedInstanceManager
if (manager?.type !== 'user') return undefined
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(manager.name)}`)
return () => router.push(`/user/${encodeURIComponent(manager.name)}`)
})
const playtimeLabel = computed(() => {
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
@@ -19,8 +19,10 @@ import { useRouter } from 'vue-router'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { toError } from '@/helpers/errors'
import { install_duplicate_instance } from '@/helpers/install'
import { edit, edit_icon, list, remove } from '@/helpers/instance'
import { edit, edit_icon, remove } from '@/helpers/instance'
import { create_group, list_groups } from '@/helpers/instance-groups'
import { injectInstanceSettings } from '@/providers/instance-settings'
import type { GameInstance } from '../../../helpers/types'
@@ -38,7 +40,7 @@ const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
const title = ref(instance.value.name)
const icon: Ref<string | undefined> = ref(instance.value.icon_path)
const groups = ref([...instance.value.groups])
const groupIds = ref([...instance.value.group_ids])
const savingReleaseChannel = ref(false)
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.update_channel)
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
@@ -57,10 +59,7 @@ async function duplicateInstance() {
})
}
const allInstances = ref((await list()) as GameInstance[])
const availableGroups = computed(() => [
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
])
const availableGroups = ref(await list_groups())
function formatReleaseChannelLabel(channel: ReleaseChannel) {
switch (channel) {
@@ -135,28 +134,34 @@ async function setIcon() {
const editInstanceObject = computed(() => ({
name: title.value.trim().substring(0, 32) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
group_ids: groupIds.value,
}))
const toggleGroup = (group: string) => {
if (groups.value.includes(group)) {
groups.value = groups.value.filter((x) => x !== group)
const toggleGroup = (groupId: string) => {
if (groupIds.value.includes(groupId)) {
groupIds.value = groupIds.value.filter((id) => id !== groupId)
} else {
groups.value.push(group)
groupIds.value.push(groupId)
}
}
const addCategory = () => {
const addCategory = async () => {
const text = newCategoryInput.value.trim()
if (text.length > 0) {
groups.value.push(text.substring(0, 32))
newCategoryInput.value = ''
try {
const group = await create_group(text.substring(0, 128))
availableGroups.value.push(group)
groupIds.value.push(group.id)
newCategoryInput.value = ''
} catch (error) {
handleError(toError(error))
}
}
}
watch(
[title, groups, groups],
[title, groupIds],
async () => {
if (removing.value) return
await edit(instance.value.id, editInstanceObject.value).catch(handleError)
@@ -369,15 +374,16 @@ const messages = defineMessages({
<div class="flex flex-col gap-1">
<Checkbox
v-for="group in availableGroups"
:key="group"
:model-value="groups.includes(group)"
:label="group"
@click="toggleGroup(group)"
:key="group.id"
:model-value="groupIds.includes(group.id)"
:label="group.name"
@click="toggleGroup(group.id)"
/>
<div class="flex gap-2 items-center">
<StyledInput
v-model="newCategoryInput"
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
:maxlength="128"
class="w-full max-w-[300px]"
@submit="() => addCategory"
/>
@@ -0,0 +1,195 @@
<template>
<FloatingActionBar
:shown="selectedInstanceCount > 0"
:aria-label="formatMessage(messages.ariaLabel)"
hide-when-modal-open
>
<div class="flex items-center gap-0.5">
<span class="px-4 py-2.5 text-base font-semibold text-contrast tabular-nums">
{{ formatMessage(messages.selectedCount, { count: selectedLibraryInstances.size }) }}
</span>
<div class="mx-1 h-6 w-px bg-surface-5" />
<ButtonStyled type="transparent">
<button
class="!text-primary"
type="button"
:disabled="busy"
@click="clearLibraryInstanceSelection"
>
<span class="bar-label">{{ formatMessage(commonMessages.clearButton) }}</span>
</button>
</ButtonStyled>
</div>
<div class="ml-auto flex items-center gap-0.5">
<ButtonStyled type="transparent">
<button type="button" :disabled="busy" @click="createGroupFromSelection">
<PlusIcon />
<span class="bar-label">{{ formatMessage(messages.createGroup) }}</span>
</button>
</ButtonStyled>
<ButtonStyled v-if="selectedGroupedInstances.length > 0" type="transparent">
<button type="button" :disabled="busy" @click="removeSelectedInstancesFromGroups">
<MinusIcon />
<span class="bar-label">{{ formatMessage(messages.removeFromGroup) }}</span>
</button>
</ButtonStyled>
<div class="mx-1 h-6 w-px bg-surface-5" />
<ButtonStyled type="transparent" color="red" color-fill="text" hover-color-fill="background">
<button
v-tooltip="deleting ? formatMessage(messages.deleting) : undefined"
type="button"
:disabled="busy"
@click="confirmDeleteModal?.show()"
>
<TrashIcon />
<span class="bar-label">{{ formatMessage(commonMessages.deleteLabel) }}</span>
</button>
</ButtonStyled>
</div>
</FloatingActionBar>
<ConfirmDeleteInstanceModal
ref="confirmDeleteModal"
:count="selectedInstanceIds.size"
@delete="deleteSelectedInstances"
/>
</template>
<script setup lang="ts">
import { MinusIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
defineMessages,
FloatingActionBar,
injectNotificationManager,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import { toError } from '@/helpers/errors'
import { edit, remove } from '@/helpers/instance'
const { formatMessage } = useVIntl()
const { handleError } = injectNotificationManager()
const {
instances,
selectedLibraryInstances,
clearLibraryInstanceSelection,
setSelectedLibraryInstances,
creatingGroup,
createDefaultGroup,
} = useLibrary()
const confirmDeleteModal = ref<InstanceType<typeof ConfirmDeleteInstanceModal>>()
const deleting = ref(false)
const removingFromGroup = ref(false)
const selectedInstanceCount = computed(() => selectedLibraryInstances.value.size)
const selectedInstanceIds = computed(
() =>
new Set([...selectedLibraryInstances.value.values()].map((selection) => selection.instanceId)),
)
const selectedGroupedInstances = computed(() =>
[...selectedLibraryInstances.value.values()].flatMap((selection) => {
const instance = instances.value.find((candidate) => candidate.id === selection.instanceId)
return instance?.group_ids.includes(selection.groupId) ? [{ instance, selection }] : []
}),
)
const busy = computed(() => deleting.value || removingFromGroup.value || creatingGroup.value)
const messages = defineMessages({
ariaLabel: {
id: 'app.library.selection.aria-label',
defaultMessage: 'Selected instances',
},
selectedCount: {
id: 'app.library.selection.selected-count',
defaultMessage: '{count} selected',
},
deleting: {
id: 'app.library.selection.deleting',
defaultMessage: 'Deleting selected instances',
},
createGroup: {
id: 'app.library.selection.create-group',
defaultMessage: 'Create group',
},
removeFromGroup: {
id: 'app.library.selection.remove-from-group',
defaultMessage: 'Remove from group',
},
})
async function createGroupFromSelection() {
if (busy.value) return
await createDefaultGroup(selectedInstanceIds.value)
}
async function removeSelectedInstancesFromGroups() {
if (busy.value || selectedGroupedInstances.value.length === 0) return
removingFromGroup.value = true
const selectedGroupIdsByInstanceId = new Map<string, Set<string>>()
for (const { instance, selection } of selectedGroupedInstances.value) {
const groupIds = selectedGroupIdsByInstanceId.get(instance.id) ?? new Set()
groupIds.add(selection.groupId)
selectedGroupIdsByInstanceId.set(instance.id, groupIds)
}
const operations = [...selectedGroupIdsByInstanceId].flatMap(([instanceId, groupIds]) => {
const instance = instances.value.find((candidate) => candidate.id === instanceId)
return instance ? [{ instance, groupIds }] : []
})
const results = await Promise.allSettled(
operations.map(({ instance, groupIds }) => {
return edit(instance.id, {
group_ids: instance.group_ids.filter((groupId) => !groupIds.has(groupId)),
})
}),
)
const nextSelectedInstances = new Map(selectedLibraryInstances.value)
for (const [index, result] of results.entries()) {
if (result.status === 'rejected') {
handleError(toError(result.reason))
} else {
for (const groupId of operations[index].groupIds) {
nextSelectedInstances.delete(
getLibraryInstanceSelectionKey({
instanceId: operations[index].instance.id,
groupId,
}),
)
}
}
}
setSelectedLibraryInstances(nextSelectedInstances.values())
removingFromGroup.value = false
}
async function deleteSelectedInstances() {
if (busy.value || selectedInstanceCount.value === 0) return
deleting.value = true
const instanceIds = [...selectedInstanceIds.value]
const results = await Promise.allSettled(instanceIds.map((instanceId) => remove(instanceId)))
const deletedInstanceIds = new Set<string>()
for (const [index, result] of results.entries()) {
if (result.status === 'rejected') {
handleError(toError(result.reason))
} else {
deletedInstanceIds.add(instanceIds[index])
}
}
setSelectedLibraryInstances(
[...selectedLibraryInstances.value.values()].filter(
(selection) => !deletedInstanceIds.has(selection.instanceId),
),
)
deleting.value = false
}
</script>
@@ -0,0 +1,144 @@
<script setup lang="ts">
import {
ClipboardCopyIcon,
EyeIcon,
FolderOpenIcon,
MinusIcon,
PlayIcon,
PlusIcon,
StopCircleIcon,
TrashIcon,
} from '@modrinth/assets'
import { computed, ref, toRef, watch } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import InstanceGroup from '@/components/ui/library/instance-group/index.vue'
import InstanceGroupDnd from '@/components/ui/library/instance-group/instance-group-dnd.vue'
import LibraryToolbar from '@/components/ui/library/library-toolbar/index.vue'
import LibrarySelectionActionBar from '@/components/ui/library/LibrarySelectionActionBar.vue'
import { getLibraryInstanceSelectionKey, provideLibrary } from '@/components/ui/library/use-library'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import type { GameInstance } from '@/helpers/types'
const props = defineProps<{
instances: GameInstance[]
}>()
const {
instanceGroups,
filters,
deleteInstance,
handleInstanceOption,
selectedLibraryInstances,
setSelectedLibraryInstances,
toggleLibraryInstanceSelection,
} = provideLibrary(toRef(props, 'instances'))
const hasActiveFilters = computed(() =>
Object.values(filters.value).some((selectedValues) => selectedValues.length > 0),
)
const visibleInstanceGroups = computed(() =>
instanceGroups.value.filter(
(instanceGroup) =>
instanceGroup.instances.length > 0 ||
(!hasActiveFilters.value && instanceGroup.key !== 'None'),
),
)
const anchorInstance = ref<{ groupId: string; instanceId: string } | null>(null)
function handleToggleInstance(groupId: string, instanceId: string, shiftKey: boolean) {
const displayedInstances = visibleInstanceGroups.value.flatMap((group) =>
group.instances.map((instance) => ({
groupId: group.id,
instanceId: instance.id,
})),
)
const anchor = anchorInstance.value
if (shiftKey && anchor && displayedInstances.length) {
const anchorIndex = displayedInstances.findIndex(
(instance) =>
instance.groupId === anchor.groupId && instance.instanceId === anchor.instanceId,
)
const targetIndex = displayedInstances.findIndex(
(instance) => instance.groupId === groupId && instance.instanceId === instanceId,
)
if (anchorIndex === -1 || targetIndex === -1) {
toggleLibraryInstanceSelection({ groupId, instanceId })
return
}
const start = Math.min(anchorIndex, targetIndex)
const end = Math.max(anchorIndex, targetIndex)
const range = displayedInstances.slice(start, end + 1)
const nextSelectedInstances = new Map(selectedLibraryInstances.value)
const targetKey = getLibraryInstanceSelectionKey({ groupId, instanceId })
if (nextSelectedInstances.has(targetKey)) {
for (const instance of range) {
nextSelectedInstances.delete(getLibraryInstanceSelectionKey(instance))
}
} else {
for (const instance of range) {
nextSelectedInstances.set(getLibraryInstanceSelectionKey(instance), instance)
}
}
setSelectedLibraryInstances(nextSelectedInstances.values())
anchorInstance.value = null
return
}
toggleLibraryInstanceSelection({ groupId, instanceId })
anchorInstance.value = { groupId, instanceId }
}
watch(
() => selectedLibraryInstances.value.size,
(selectedInstanceCount) => {
if (selectedInstanceCount === 0) {
anchorInstance.value = null
}
},
)
</script>
<template>
<InstanceGroupDnd :instances="instances">
<section class="flex flex-col gap-3 pb-16">
<h2 class="m-0 text-2xl font-semibold text-contrast">Library</h2>
<LibraryToolbar />
<div class="flex flex-col">
<InstanceGroup
v-for="instanceGroup in visibleInstanceGroups"
:key="instanceGroup.id"
:hide-header="instanceGroup.key === 'None' && visibleInstanceGroups.length === 1"
:instance-group="instanceGroup"
:selection-anchor-instance-id="
anchorInstance?.groupId === instanceGroup.id ? anchorInstance.instanceId : null
"
@toggle-selection="
(instanceId: string, shiftKey: boolean) =>
handleToggleInstance(instanceGroup.id, instanceId, shiftKey)
"
/>
</div>
</section>
</InstanceGroupDnd>
<LibrarySelectionActionBar />
<ConfirmDeleteInstanceModal ref="confirmDeleteModal" @delete="deleteInstance" />
<ContextMenu ref="instanceOptions" @option-clicked="handleInstanceOption">
<template #play> <PlayIcon /> Play </template>
<template #stop> <StopCircleIcon /> Stop </template>
<template #add_content> <PlusIcon /> Add content </template>
<template #edit> <EyeIcon /> View instance </template>
<template #duplicate> <ClipboardCopyIcon /> Duplicate instance</template>
<template #delete> <TrashIcon /> Delete </template>
<template #open> <FolderOpenIcon /> Open folder </template>
<template #copy> <ClipboardCopyIcon /> Copy path </template>
<template #remove_from_group> <MinusIcon /> Remove from group </template>
</ContextMenu>
</template>
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import DragPreview from '@/components/ui/library/instance-group/drag-preview.vue'
import type { InstanceDragGatherItem } from '@/components/ui/library/instance-group/use-instance-drag-gather'
const props = defineProps<{
items: InstanceDragGatherItem[]
target: {
x: number
y: number
}
}>()
const emit = defineEmits<{
complete: []
}>()
const gatherDuration = 500
const gathered = ref(false)
const finishing = ref(false)
let animationFrame: number | undefined
let completionTimer: number | undefined
const itemContainerStyles = computed(() =>
props.items.map((_, index) => ({
left: `${props.target.x}px`,
top: `${props.target.y}px`,
zIndex: `${index}`,
})),
)
const itemStyles = computed(() =>
props.items.map((item, index) => {
const originX = item.rect.left - props.target.x
const originY = item.rect.top - props.target.y
return {
width: `${item.rect.width}px`,
height: `${item.rect.height}px`,
transform: gathered.value
? `translate3d(${-item.rect.width / 2}px, ${-item.rect.height / 2}px, 0)`
: `translate3d(${originX}px, ${originY}px, 0)`,
transitionDuration: `${gatherDuration}ms`,
transitionDelay: `${Math.min(index, 8) * 10}ms`,
}
}),
)
onMounted(() => {
animationFrame = requestAnimationFrame(() => {
animationFrame = requestAnimationFrame(() => {
gathered.value = true
completionTimer = window.setTimeout(
() => {
finishing.value = true
emit('complete')
},
Math.max(gatherDuration - 250, 0),
)
})
})
})
onBeforeUnmount(() => {
if (animationFrame !== undefined) {
cancelAnimationFrame(animationFrame)
}
if (completionTimer !== undefined) {
clearTimeout(completionTimer)
}
})
</script>
<template>
<Teleport to="body">
<div
class="pointer-events-none fixed inset-0 z-[9998] overflow-hidden transition-opacity duration-150 ease-out"
:class="finishing ? 'opacity-0' : 'opacity-100'"
aria-hidden="true"
>
<div
v-for="(item, index) in items"
:key="item.instance.id"
class="fixed"
:style="itemContainerStyles[index]"
>
<div
class="origin-center transition-transform ease-[cubic-bezier(0.2,0.8,0.2,1)]"
:style="itemStyles[index]"
>
<DragPreview :instance="item.instance" />
</div>
</div>
</div>
</Teleport>
</template>
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { TagItem } from '@modrinth/ui'
import InstanceCardView from '@/components/ui/library/instance-group/instance-card-view.vue'
import type { GameInstance } from '@/helpers/types'
withDefaults(
defineProps<{
instance: GameInstance
count?: number
}>(),
{
count: 1,
},
)
</script>
<template>
<div aria-hidden="true" class="relative w-full select-none">
<div
v-if="count > 1"
class="absolute inset-x-3 -bottom-2 top-2 rounded-[20px] border border-solid border-surface-5 bg-surface-2 opacity-60 shadow-md"
/>
<div
v-if="count > 1"
class="absolute inset-x-1.5 -bottom-1 top-1 rounded-[20px] border border-solid border-surface-4 bg-surface-3 opacity-80 shadow-md"
/>
<InstanceCardView :instance="instance" class="opacity-90 shadow-lg">
<template #overlay>
<TagItem
v-if="count > 1"
class="!absolute right-3 top-3 z-[2] border-surface-5 bg-surface-2 font-semibold tabular-nums text-contrast"
>
{{ count }}
</TagItem>
</template>
</InstanceCardView>
</div>
</template>
@@ -0,0 +1,422 @@
<script setup lang="ts">
import { useDroppable } from '@dnd-kit/vue'
import { DropdownIcon, EditIcon, TrashIcon, XIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
InlineEditableText,
NewModal,
TagItem,
useVIntl,
} from '@modrinth/ui'
import { computed, nextTick, ref, watch } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import InstanceCard from '@/components/ui/library/instance-group/instance-card.vue'
import type {
InstanceCard as InstanceCardExposed,
InstanceGroup as InstanceGroupType,
} from '@/components/ui/library/use-library'
import { useLibrary } from '@/components/ui/library/use-library'
const props = withDefaults(
defineProps<{
hideHeader?: boolean
instanceGroup: InstanceGroupType
selectionAnchorInstanceId?: string | null
}>(),
{
hideHeader: false,
selectionAnchorInstanceId: null,
},
)
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()
const {
isSectionCollapsed,
setSectionCollapsed,
deleteGroup,
renameGroup,
groupIdPendingNameEdit,
completePendingGroupNameEdit,
handleInstanceContextMenu,
displayState,
activeInstanceGroupDrag,
instanceGroupDragTarget,
getInstanceGroupDropState,
} = useLibrary()
const instanceComponents = new Map<string, InstanceCardExposed>()
const groupDropTarget = ref<HTMLElement>()
const groupAccordion = ref<InstanceType<typeof Accordion>>()
const groupOptions = ref<InstanceType<typeof ContextMenu>>()
const groupNameInput = ref<InstanceType<typeof InlineEditableText>>()
const confirmDeleteGroupModal = ref<InstanceType<typeof NewModal>>()
const deletingGroup = ref(false)
const groupName = ref(props.instanceGroup.key)
const isUngrouped = computed(() => props.instanceGroup.id === 'group:none')
const groupContextMenuOpen = ref(false)
const isGroupToggleBlocked = computed(
() => groupContextMenuOpen.value || Boolean(groupNameInput.value?.isEditing),
)
let shouldSkipGroupToggle = false
let groupToggleEventToSkip: MouseEvent | undefined
const emit = defineEmits<{
(e: 'toggle-selection', instanceId: string, shiftKey: boolean): void
}>()
useDroppable({
id: computed(() => `instance-group:${props.instanceGroup.id}`),
element: groupDropTarget,
disabled: computed(() => displayState.value.group !== 'Group'),
data: computed(() => ({
groupId: props.instanceGroup.id,
})),
})
const messages = defineMessages({
ungrouped: {
id: 'app.library.group.ungrouped',
defaultMessage: 'Ungrouped',
},
deleteGroup: {
id: 'app.library.group.delete',
defaultMessage: 'Delete group',
},
editGroupName: {
id: 'app.library.group.edit-name',
defaultMessage: 'Edit group name',
},
renameGroupFailed: {
id: 'app.library.group.rename-failed',
defaultMessage: 'Unable to rename group',
},
groupNameEmpty: {
id: 'app.library.group.name-empty',
defaultMessage: 'Group names cannot be empty.',
},
groupNameTooLong: {
id: 'app.library.group.name-too-long',
defaultMessage: 'Group names cannot be longer than 32 characters.',
},
groupNameReserved: {
id: 'app.library.group.name-reserved',
defaultMessage: '"None" is reserved and cannot be used as a group name.',
},
deleteGroupDescription: {
id: 'app.library.group.delete-description',
defaultMessage: 'Instances in this group will be ungrouped.',
},
})
function openInstanceContextMenu(event: MouseEvent, instanceId: string, instanceGroupId: string) {
const instanceComponent = instanceComponents.get(instanceId)
if (!instanceComponent) return
handleInstanceContextMenu(event, instanceComponent, instanceGroupId)
}
function setInstanceComponent(instanceId: string, component: unknown) {
if (component) {
instanceComponents.set(instanceId, component as InstanceCardExposed)
} else {
instanceComponents.delete(instanceId)
}
}
async function removeGroup() {
if (deletingGroup.value) return
deletingGroup.value = true
const deleted = await deleteGroup(props.instanceGroup.id)
deletingGroup.value = false
if (deleted) {
confirmDeleteGroupModal.value?.hide()
}
}
function requestGroupDeletion() {
if (isUngrouped.value) return
if (props.instanceGroup.instances.length > 0) {
confirmDeleteGroupModal.value?.show()
} else {
void removeGroup()
}
}
function openGroupContextMenu(event: MouseEvent) {
if (isUngrouped.value) return
groupContextMenuOpen.value = true
groupOptions.value?.showMenu(event, props.instanceGroup, [
{ name: 'edit_name' },
{ type: 'divider' },
{ name: 'delete_group', color: 'danger' },
])
}
function handleGroupOption({ option }: { option: string }) {
if (option === 'edit_name') {
void groupNameInput.value?.startEditing()
return
}
if (option === 'delete_group') {
requestGroupDeletion()
}
}
function prepareGroupToggle(event: PointerEvent) {
const editor = groupNameInput.value
const contextMenuWasOpen = groupContextMenuOpen.value
if (contextMenuWasOpen) {
groupOptions.value?.hideMenu()
}
shouldSkipGroupToggle = Boolean(
contextMenuWasOpen ||
(editor?.isEditing && event.target instanceof Node && !editor.$el.contains(event.target)),
)
}
function captureGroupClick(event: MouseEvent) {
groupToggleEventToSkip = shouldSkipGroupToggle ? event : undefined
shouldSkipGroupToggle = false
}
function toggleGroup(event: MouseEvent) {
if (event === groupToggleEventToSkip) {
groupToggleEventToSkip = undefined
return
}
if (groupAccordion.value?.isOpen) {
groupAccordion.value.close()
} else {
groupAccordion.value?.open()
}
}
function validateGroupName(value: string) {
const normalizedGroupName = value.trim()
let reason: string | undefined
if (normalizedGroupName.length === 0) {
reason = formatMessage(messages.groupNameEmpty)
} else if (normalizedGroupName.length > 32) {
reason = formatMessage(messages.groupNameTooLong)
} else if (normalizedGroupName.toLowerCase() === 'none') {
reason = formatMessage(messages.groupNameReserved)
}
if (reason) {
addNotification({
type: 'error',
title: formatMessage(messages.renameGroupFailed),
text: reason,
})
return false
}
return true
}
async function updateGroupName(value: string) {
return await renameGroup(props.instanceGroup.id, value)
}
watch(
() => props.instanceGroup.key,
(value) => {
groupName.value = value
},
)
watch(
[() => props.instanceGroup.id, groupIdPendingNameEdit],
async ([groupId, pendingGroupId]) => {
if (groupId !== pendingGroupId) return
await nextTick()
await groupNameInput.value?.startEditing()
completePendingGroupNameEdit(groupId)
},
{ immediate: true, flush: 'post' },
)
watch(
() => props.hideHeader,
(hideHeader) => {
if (hideHeader) {
groupAccordion.value?.open()
}
},
{ flush: 'post' },
)
</script>
<template>
<div
ref="groupDropTarget"
class="instance-group group/instance-container relative select-none pb-3"
@contextmenu.prevent.stop="openGroupContextMenu"
>
<Transition
enter-active-class="transition-opacity duration-150 ease-out"
enter-from-class="!opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition-opacity duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="!opacity-0"
>
<div
v-if="
activeInstanceGroupDrag &&
instanceGroupDragTarget === instanceGroup.id &&
getInstanceGroupDropState(instanceGroup.id).canDrop
"
class="pointer-events-none absolute -inset-2 inset-y-0 z-20 rounded-xl border-2 opacity-40 border-dashed border-contrast bg-transparent"
/>
</Transition>
<div
v-if="!hideHeader"
class="group/header h-10 flex w-full items-center gap-2 border-0 border-b border-solid border-b-surface-5"
>
<div
class="group/open-target flex min-w-0 items-center gap-2"
:class="isGroupToggleBlocked ? 'cursor-default' : 'cursor-pointer'"
@click="toggleGroup"
@click.capture="captureGroupClick"
@pointerdown.capture="prepareGroupToggle"
>
<button
class="flex shrink-0 items-center border-0 bg-transparent p-0"
:class="isGroupToggleBlocked ? 'cursor-default' : 'cursor-pointer'"
type="button"
:aria-expanded="groupAccordion?.isOpen"
:aria-label="groupAccordion?.isOpen ? 'Collapse group' : 'Expand group'"
@click.stop="toggleGroup"
>
<DropdownIcon
class="size-5 shrink-0 text-secondary transition-all duration-300 group-hover/open-target:text-primary"
:class="{ 'rotate-180': groupAccordion?.isOpen }"
/>
</button>
<InlineEditableText
v-if="!isUngrouped"
ref="groupNameInput"
v-model="groupName"
activation-mode="icon"
class="text-base font-semibold !h-10 text-primary select-none group-hover/open-target:text-contrast"
:edit-label="formatMessage(commonMessages.renameButton)"
max-width="24rem"
icon-text-class="select-none"
:max-length="32"
:on-change="updateGroupName"
:validate="validateGroupName"
/>
<span
v-else
class="text-base font-semibold text-primary select-none group-hover/open-target:text-contrast"
>
{{ formatMessage(messages.ungrouped) }}
</span>
<TagItem
v-if="instanceGroup.instances.length"
class="shrink-0 border-surface-3 bg-surface-2"
>
{{ instanceGroup.instances.length }}
</TagItem>
</div>
<div class="min-w-0 flex-1" />
<ButtonStyled v-if="!isUngrouped" circular type="transparent">
<button
v-tooltip="formatMessage(messages.deleteGroup)"
class="opacity-0 !transition-all duration-150 group-hover/instance-container:opacity-100 -m-1.5"
type="button"
:aria-label="formatMessage(messages.deleteGroup)"
:disabled="deletingGroup"
@click.stop="requestGroupDeletion"
>
<TrashIcon class="!size-4 !min-h-4 !min-w-4" />
</button>
</ButtonStyled>
</div>
<Accordion
ref="groupAccordion"
:open-by-default="hideHeader || !isSectionCollapsed(instanceGroup.id)"
class="w-full"
@on-open="setSectionCollapsed(instanceGroup.id, false)"
@on-close="setSectionCollapsed(instanceGroup.id, true)"
>
<section
class="grid min-h-[45px] mt-2.5 w-full grid-cols-[repeat(auto-fill,minmax(min(20rem,100%),1fr))] gap-3 overflow-y-auto scroll-smooth"
>
<div v-for="instance in instanceGroup.instances" :key="instance.id" class="min-w-0 w-full">
<InstanceCard
:ref="(component: unknown) => setInstanceComponent(instance.id, component)"
:instance="instance"
:instance-group-id="instanceGroup.id"
:is-selection-anchor="selectionAnchorInstanceId === instance.id"
@toggle-selection="
(shiftKey: boolean) => emit('toggle-selection', instance.id, shiftKey)
"
@contextmenu.prevent.stop="
(event: MouseEvent) => openInstanceContextMenu(event, instance.id, instanceGroup.id)
"
/>
</div>
<p
v-if="instanceGroup.instances.length === 0"
class="col-span-full m-0 pt-1 pl-0.5 text-base font-base text-secondary opacity-80"
>
This group is empty. Drag and drop to add instances.
</p>
</section>
</Accordion>
</div>
<ContextMenu
ref="groupOptions"
@menu-closed="groupContextMenuOpen = false"
@option-clicked="handleGroupOption"
>
<template #edit_name> <EditIcon /> {{ formatMessage(messages.editGroupName) }} </template>
<template #delete_group> <TrashIcon /> {{ formatMessage(messages.deleteGroup) }} </template>
</ContextMenu>
<NewModal
ref="confirmDeleteGroupModal"
:header="formatMessage(messages.deleteGroup)"
fade="danger"
width="500px"
>
<p class="m-0 text-base text-primary">
{{ formatMessage(messages.deleteGroupDescription) }}
</p>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button type="button" @click="confirmDeleteGroupModal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button type="button" :disabled="deletingGroup" @click="removeGroup">
<TrashIcon />
{{ formatMessage(messages.deleteGroup) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { PageRoundIcon } from '@modrinth/assets'
import { Avatar } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, type CSSProperties, ref } from 'vue'
import InstanceFileIcon from '@/assets/icons/instance-file.svg'
import type { GameInstance } from '@/helpers/types'
const avatarSize = '84px'
const avatarStyles: CSSProperties = {
top: '-26px',
right: '20px',
width: avatarSize,
height: avatarSize,
borderRadius: '22px 22px 38px 22px',
}
const avatar = ref<InstanceType<typeof Avatar> | null>(null)
const props = withDefaults(
defineProps<{
instance: GameInstance
selected?: boolean
}>(),
{
selected: false,
},
)
const instanceType = computed(() => {
if (
props.instance.link?.type === 'server_project' ||
props.instance.link?.type === 'server_project_modpack'
) {
return 'SRV'
}
return props.instance.link?.type === 'modrinth_modpack' ? 'MPK' : 'CST'
})
const iconSrc = computed(() =>
props.instance.icon_path ? convertFileSrc(props.instance.icon_path) : undefined,
)
const avatarFailed = computed(() => avatar.value?.failed ?? false)
const hasVisibleIcon = computed(() => Boolean(iconSrc.value) && !avatarFailed.value)
</script>
<template>
<div
class="relative flex min-h-[76px] w-full select-none items-center justify-center gap-2 overflow-clip rounded-[20px] border border-solid bg-surface-3 p-4 text-left transition-all"
:class="{
'[border-color:color-mix(in_srgb,var(--color-text-primary)_20%,transparent)] brightness-110':
selected,
'border-surface-4': !selected,
}"
>
<PageRoundIcon
aria-hidden="true"
class="pointer-events-none absolute -top-[52px] right-[0px] size-[124px] [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
:class="{ 'opacity-[0.03]': hasVisibleIcon, 'opacity-5': !hasVisibleIcon }"
/>
<div
v-if="hasVisibleIcon"
class="pointer-events-none absolute bg-surface-3"
:style="avatarStyles"
/>
<Avatar
v-if="iconSrc"
ref="avatar"
class="pointer-events-none !border-none ![background-color:color-mix(in_srgb,var(--color-contrast)_5%,transparent)] !p-0 !absolute opacity-50 [mask-image:linear-gradient(135deg,transparent_16%,black_100%)]"
:class="{ hidden: avatarFailed }"
:style="avatarStyles"
:size="avatarSize"
:src="iconSrc"
:tint-by="instance.id"
alt=""
no-shadow
/>
<div class="relative z-[1] flex min-w-0 flex-1 items-center gap-2 pr-20">
<slot name="leading" :instance-type="instanceType">
<div
class="flex size-10 shrink-0 flex-col items-center gap-px overflow-clip rounded-[14px] px-[3px] py-0.5 text-primary"
>
<InstanceFileIcon class="h-[21px] w-[31px] shrink-0 text-primary [&_path]:fill-current" />
<span class="h-3.5 text-sm font-extrabold leading-[13px]">
{{ instanceType }}
</span>
</div>
</slot>
<div class="flex min-w-0 flex-1 flex-col justify-center gap-1">
<p class="m-0 truncate text-base font-semibold leading-5 text-contrast">
{{ instance.name }}
</p>
<p class="m-0 truncate text-sm font-medium capitalize leading-[18px] text-primary">
{{ instance.loader }} {{ instance.game_version }}
</p>
</div>
</div>
<slot name="overlay" />
</div>
</template>
@@ -0,0 +1,318 @@
<script setup lang="ts">
import { useDraggable } from '@dnd-kit/vue'
import { CheckIcon, DownloadIcon, PlayIcon, SpinnerIcon, StopCircleIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { useMagicKeys } from '@vueuse/core'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import InstanceFileIcon from '@/assets/icons/instance-file.svg'
import InstanceCardView from '@/components/ui/library/instance-group/instance-card-view.vue'
import { getLibraryInstanceSelectionKey, useLibrary } from '@/components/ui/library/use-library'
import { trackEvent } from '@/helpers/analytics'
import { process_listener } from '@/helpers/events'
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
import { showInstanceInFolder } from '@/helpers/utils.js'
import { handleSevereError } from '@/store/error.js'
type ProcessEvent = 'installing' | 'launched' | 'finished'
type ProcessEventPayload = {
instance_id: string
event: ProcessEvent
}
const { handleError } = injectNotificationManager()
const {
displayState,
selectedLibraryInstances,
isLibraryInstanceSelectionActive,
activeDraggedInstanceKeys,
} = useLibrary()
const props = defineProps<{
instance: GameInstance
instanceGroupId: string
isSelectionAnchor?: boolean
}>()
const emit = defineEmits<{
(e: 'toggle-selection', shiftKey: boolean): void
}>()
const instanceCardElement = ref<InstanceType<typeof InstanceCardView> | null>(null)
const playing = ref(false)
const loading = ref(false)
const currentEvent = ref<ProcessEvent | null>(null)
const modLoading = computed(
() =>
loading.value ||
currentEvent.value === 'installing' ||
(currentEvent.value === 'launched' && !playing.value),
)
const installing = computed(() => props.instance.install_stage.includes('installing'))
const installed = computed(() => props.instance.install_stage === 'installed')
const selectionKey = computed(() =>
getLibraryInstanceSelectionKey({
instanceId: props.instance.id,
groupId: props.instanceGroupId,
}),
)
const selected = computed(() => selectedLibraryInstances.value.has(selectionKey.value))
const keys = useMagicKeys()
const holdingShift = computed(() => keys.shift.value)
const isPartOfActiveDrag = computed(() => activeDraggedInstanceKeys.value.has(selectionKey.value))
const { isDragging } = useDraggable({
id: computed(() => `instance:${props.instanceGroupId}:${props.instance.id}`),
element: instanceCardElement,
disabled: computed(() => displayState.value.group !== 'Group'),
data: computed(() => ({
instanceId: props.instance.id,
fromGroup: props.instanceGroupId,
})),
})
const router = useRouter()
const seeInstance = async () => {
await router.push(`/instance/${encodeURIComponent(props.instance.id)}`)
}
const toggleSelection = (event?: MouseEvent) => {
emit('toggle-selection', event?.shiftKey ?? false)
}
const activateCard = (event: MouseEvent) => {
if (isLibraryInstanceSelectionActive.value || event.shiftKey) {
toggleSelection(event)
} else {
void seeInstance()
}
}
const handleCardKeydown = (event: KeyboardEvent) => {
if (event.target !== event.currentTarget) return
if (event.key === 'Enter') {
event.preventDefault()
if (isLibraryInstanceSelectionActive.value) {
toggleSelection()
} else {
void seeInstance()
}
} else if (event.key === ' ' && isLibraryInstanceSelectionActive.value) {
event.preventDefault()
toggleSelection()
}
}
const checkProcess = async () => {
const runningProcesses = (await get_by_instance_id(props.instance.id).catch(handleError)) ?? []
playing.value = runningProcesses.length > 0
}
const play = async (event: MouseEvent | null, context: string) => {
event?.stopPropagation()
if (props.instance.quarantined) return
loading.value = true
await run(props.instance.id)
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
.finally(() => {
trackEvent('InstanceStart', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
})
loading.value = false
}
const stop = async (event: MouseEvent | null, context: string) => {
event?.stopPropagation()
playing.value = false
await kill(props.instance.id).catch(handleError)
trackEvent('InstanceStop', {
loader: props.instance.loader,
game_version: props.instance.game_version,
source: context,
})
}
const repair = async (event: MouseEvent) => {
event.stopPropagation()
if (props.instance.quarantined) return
if (
props.instance.install_stage !== 'pack_installed' &&
(props.instance.link?.type === 'modrinth_modpack' ||
props.instance.link?.type === 'server_project_modpack')
) {
await install_pack_to_existing_instance(props.instance.id, {
type: 'fromVersionId',
project_id: props.instance.link.project_id ?? props.instance.link.server_project_id ?? '',
version_id: props.instance.link.version_id ?? props.instance.link.content_version_id ?? '',
title: props.instance.name,
}).catch(handleError)
} else {
await install_existing_instance(props.instance.id, false).catch(handleError)
}
}
const openFolder = async () => {
await showInstanceInFolder(props.instance.id)
}
const addContent = async () => {
if (props.instance.quarantined) return
await router.push({
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.id },
})
}
defineExpose({
play,
stop,
seeInstance,
openFolder,
addContent,
instance: props.instance,
})
const unlisten = await process_listener((event: ProcessEventPayload) => {
if (event.instance_id === props.instance.id) {
currentEvent.value = event.event
if (event.event === 'finished') {
playing.value = false
}
}
})
onMounted(() => {
checkProcess()
})
onUnmounted(() => unlisten())
</script>
<template>
<InstanceCardView
ref="instanceCardElement"
class="group/card cursor-pointer -outline-offset-2 focus-visible:!outline-2 hover:brightness-110"
:class="{
'!scale-100': isDragging,
'opacity-50': isPartOfActiveDrag,
'[&:active:not(:has(.selection-button:active))]:scale-[0.95]':
!isLibraryInstanceSelectionActive,
}"
:instance="instance"
:selected="selected"
data-library-instance-card
:data-instance-id="instance.id"
:data-instance-group="instanceGroupId"
role="button"
tabindex="0"
:aria-label="
isLibraryInstanceSelectionActive
? `${selected ? 'Deselect' : 'Select'} ${instance.name}`
: `Open ${instance.name}`
"
:aria-pressed="isLibraryInstanceSelectionActive ? selected : undefined"
@click="activateCard"
@keydown="handleCardKeydown"
@mouseenter="checkProcess"
>
<template #leading="{ instanceType }">
<div class="relative flex size-10 shrink-0 items-center justify-center">
<div
v-if="!playing && !modLoading && !installing"
class="flex w-10 flex-col items-center gap-px overflow-clip rounded-[14px] px-[3px] py-0.5 text-primary transition-opacity"
:class="{
'group-hover/card:scale-75 group-hover/card:opacity-0':
!instance.quarantined && !isLibraryInstanceSelectionActive,
}"
>
<InstanceFileIcon
class="h-[21px] w-[31px] shrink-0 text-primary [&_path]:fill-current"
/>
<span class="h-3.5 text-sm font-extrabold leading-[13px]">
{{ instanceType }}
</span>
</div>
<div class="absolute inset-0 flex items-center justify-center">
<ButtonStyled v-if="playing" color="red" circular>
<button
v-tooltip="'Stop'"
class="card-shadow"
@click="(e) => stop(e, 'InstanceCard')"
@mouseenter="checkProcess"
>
<StopCircleIcon />
</button>
</ButtonStyled>
<SpinnerIcon
v-else-if="modLoading || installing"
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
class="size-8 animate-spin"
tabindex="-1"
/>
<ButtonStyled
v-else-if="!isLibraryInstanceSelectionActive && !installed && !instance.quarantined"
color="brand"
circular
>
<button
v-tooltip="'Repair'"
class="card-shadow origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
@click="(e) => repair(e)"
>
<DownloadIcon />
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="!isLibraryInstanceSelectionActive && !instance.quarantined"
color="brand"
circular
>
<button
v-tooltip="'Play'"
class="card-shadow origin-bottom scale-75 opacity-0 transition-opacity group-hover/card:scale-100 group-hover/card:opacity-100"
@click="(e) => play(e, 'InstanceCard')"
@mouseenter="checkProcess"
>
<PlayIcon class="translate-x-px" />
</button>
</ButtonStyled>
</div>
</div>
</template>
<template #overlay>
<button
type="button"
class="selection-button group/selection absolute right-0 top-0 z-[2] flex size-[50px] h-full cursor-pointer items-start pt-4 justify-center border-0 bg-transparent p-0"
:aria-label="selected ? 'Deselect instance' : 'Select instance'"
:aria-pressed="selected"
@click.stop="toggleSelection"
>
<span
v-tooltip="selected ? 'Deselect instance' : 'Select instance'"
class="relative flex size-[24px] items-center justify-center rounded-full opacity-0 transition-opacity duration-200 ease-out group-hover/card:opacity-100 group-hover/selection:brightness-125"
:class="{
'border-0 !opacity-100': selected,
'border-2 border-solid border-primary bg-transparent': !selected,
'[outline:3px_solid_var(--color-purple)] outline-offset-1':
holdingShift && isSelectionAnchor,
}"
>
<span v-if="selected" class="absolute inset-0 rounded-full bg-primary" />
<CheckIcon v-if="selected" class="relative size-4 invert [stroke-width:3] top-px" />
</span>
</button>
</template>
</InstanceCardView>
</template>
@@ -0,0 +1,190 @@
<script setup lang="ts">
import {
DragDropProvider,
type DragEndEvent,
type DragMoveEvent,
type DragOverEvent,
DragOverlay,
type DragStartEvent,
} from '@dnd-kit/vue'
import { computed, onBeforeUnmount, ref, toRef, watch } from 'vue'
import DragGather from '@/components/ui/library/instance-group/drag-gather.vue'
import DragPreview from '@/components/ui/library/instance-group/drag-preview.vue'
import { useInstanceDragGather } from '@/components/ui/library/instance-group/use-instance-drag-gather'
import { useLibrary } from '@/components/ui/library/use-library'
import type { GameInstance } from '@/helpers/types'
type InstanceDragData = {
instanceId: string
fromGroup: string
}
type InstanceGroupDndDropData = {
groupId: string
}
const props = defineProps<{
instances: GameInstance[]
}>()
const {
activeInstanceGroupDrag,
instanceGroupDragPointer,
instanceGroupDragStatus,
startInstanceGroupDrag,
updateInstanceGroupDrag,
finishInstanceGroupDrag,
setInstanceGroupDragTarget,
getInstanceGroupDropState,
moveDraggedInstancesToGroup,
} = useLibrary()
const draggedInstances = computed(() => {
const drag = activeInstanceGroupDrag.value
if (!drag) return []
const draggedInstanceIds = new Set(drag.instances.map((selection) => selection.instanceId))
return props.instances.filter((instance) => draggedInstanceIds.has(instance.id))
})
const draggedInstance = computed(() => {
const drag = activeInstanceGroupDrag.value
return drag
? props.instances.find((instance) => instance.id === drag.primaryInstanceId)
: undefined
})
const {
items: gatherItems,
target: gatherTarget,
isGathering,
start: startGather,
updateTarget: updateGatherTarget,
clear: clearGather,
finish: finishGather,
} = useInstanceDragGather(toRef(props, 'instances'))
const instanceGroupDragStatusPopover = ref<HTMLElement>()
let statusPopoverFrame: number | undefined
const instanceGroupDragStatusStyle = computed(() => ({
left: `${Math.min(instanceGroupDragPointer.value.x + 4, window.innerWidth - 220)}px`,
top: `${Math.min(instanceGroupDragPointer.value.y + 4, window.innerHeight - 48)}px`,
}))
watch(
activeInstanceGroupDrag,
(drag) => {
if (statusPopoverFrame !== undefined) {
cancelAnimationFrame(statusPopoverFrame)
statusPopoverFrame = undefined
}
const popover = instanceGroupDragStatusPopover.value
if (!drag) {
if (popover?.matches(':popover-open')) {
popover.hidePopover()
}
return
}
statusPopoverFrame = requestAnimationFrame(() => {
statusPopoverFrame = undefined
const currentPopover = instanceGroupDragStatusPopover.value
if (activeInstanceGroupDrag.value && !currentPopover?.matches(':popover-open')) {
currentPopover?.showPopover()
}
})
},
{ flush: 'sync' },
)
onBeforeUnmount(() => {
if (statusPopoverFrame !== undefined) {
cancelAnimationFrame(statusPopoverFrame)
}
})
function isAltKeyPressed(event?: Event) {
return event instanceof MouseEvent || event instanceof KeyboardEvent ? event.altKey : false
}
function handleDragStart(event: DragStartEvent) {
const sourceData = event.operation.source?.data as InstanceDragData | undefined
if (!sourceData) return
const pointer = event.operation.position.current
startInstanceGroupDrag(
sourceData.instanceId,
sourceData.fromGroup,
pointer,
isAltKeyPressed(event.nativeEvent),
)
startGather(activeInstanceGroupDrag.value, sourceData, pointer)
}
function handleDragMove(event: DragMoveEvent) {
const pointer = event.to ?? event.operation.position.current
updateInstanceGroupDrag(pointer, isAltKeyPressed(event.nativeEvent))
if (gatherItems.value.length > 0) {
updateGatherTarget(pointer)
}
}
function handleDragOver(event: DragOverEvent) {
const targetData = event.operation.target?.data as InstanceGroupDndDropData | undefined
setInstanceGroupDragTarget(targetData?.groupId ?? null)
}
function handleDragEnd(event: DragEndEvent) {
const targetData = event.operation.target?.data as InstanceGroupDndDropData | undefined
if (!event.canceled && targetData) {
const dropState = getInstanceGroupDropState(targetData.groupId)
if (dropState.canDrop) {
void moveDraggedInstancesToGroup(targetData.groupId, dropState.operation === 'add')
}
}
clearGather()
finishInstanceGroupDrag()
}
</script>
<template>
<DragDropProvider
@drag-start="handleDragStart"
@drag-move="handleDragMove"
@drag-over="handleDragOver"
@drag-end="handleDragEnd"
>
<slot />
<Teleport to="body">
<div class="pointer-events-none fixed inset-0 z-[9999]">
<DragOverlay :drop-animation="null">
<div
v-if="draggedInstance"
class="w-full transition-all duration-150 ease-out"
:class="isGathering ? 'scale-[0.975]' : 'scale-100'"
>
<DragPreview :instance="draggedInstance" :count="draggedInstances.length" />
</div>
</DragOverlay>
</div>
</Teleport>
</DragDropProvider>
<DragGather
v-if="gatherItems.length > 0"
:items="gatherItems"
:target="gatherTarget"
@complete="finishGather"
/>
<Teleport to="body">
<div
ref="instanceGroupDragStatusPopover"
popover="manual"
class="pointer-events-none fixed inset-auto z-[9999] m-0 rounded-xl border border-solid border-surface-5 bg-surface-4 px-2 py-1.5 text-sm font-semibold text-contrast empty:hidden"
:style="instanceGroupDragStatusStyle"
>
{{ instanceGroupDragStatus }}
</div>
</Teleport>
</template>
@@ -0,0 +1,128 @@
import { onBeforeUnmount, type Ref, ref } from 'vue'
import type { ActiveInstanceGroupDrag } from '@/components/ui/library/use-library'
import type { GameInstance } from '@/helpers/types'
type Point = {
x: number
y: number
}
type InstanceDragData = {
instanceId: string
fromGroup: string
}
export type InstanceDragGatherItem = {
instance: GameInstance
rect: {
left: number
top: number
width: number
height: number
}
}
export function useInstanceDragGather(instances: Ref<GameInstance[]>) {
const items = ref<InstanceDragGatherItem[]>([])
const target = ref<Point>({ x: 0, y: 0 })
const targetOffset = ref<Point>({ x: 0, y: 0 })
const isGathering = ref(false)
let cleanupTimer: number | undefined
const clearCleanupTimer = () => {
if (cleanupTimer !== undefined) {
clearTimeout(cleanupTimer)
cleanupTimer = undefined
}
}
const clear = () => {
clearCleanupTimer()
items.value = []
targetOffset.value = { x: 0, y: 0 }
isGathering.value = false
}
const updateTarget = (pointer: Point) => {
target.value = {
x: pointer.x + targetOffset.value.x,
y: pointer.y + targetOffset.value.y,
}
}
const start = (
drag: ActiveInstanceGroupDrag | null,
source: InstanceDragData,
pointer: Point,
) => {
clear()
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!drag || drag.instances.length < 2 || reduceMotion) return
const instanceCards = Array.from(
document.querySelectorAll<HTMLElement>('[data-library-instance-card]'),
)
const sourceCard = instanceCards.find(
(card) =>
card.dataset.instanceId === source.instanceId &&
card.dataset.instanceGroup === source.fromGroup,
)
if (sourceCard) {
const sourceRect = sourceCard.getBoundingClientRect()
targetOffset.value = {
x: sourceRect.left + sourceRect.width / 2 - pointer.x,
y: sourceRect.top + sourceRect.height / 2 - pointer.y,
}
}
items.value = drag.instances.flatMap(({ instanceId, groupId }) => {
const instance = instances.value.find((candidate) => candidate.id === instanceId)
const matchingCards = instanceCards.filter(
(card) => card.dataset.instanceId === instanceId && card.getClientRects().length > 0,
)
const card =
matchingCards.find((candidate) => candidate.dataset.instanceGroup === groupId) ??
matchingCards[0]
if (!instance || !card) return []
const rect = card.getBoundingClientRect()
return [
{
instance,
rect: {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
},
},
]
})
updateTarget(pointer)
isGathering.value = items.value.length > 0
}
const finish = () => {
isGathering.value = false
clearCleanupTimer()
cleanupTimer = window.setTimeout(() => {
items.value = []
cleanupTimer = undefined
}, 400)
}
onBeforeUnmount(clearCleanupTimer)
return {
items,
target,
isGathering,
start,
updateTarget,
clear,
finish,
}
}
@@ -0,0 +1,57 @@
<script setup lang="ts">
import {
DropdownFilterBar,
type DropdownFilterBarCategory,
formatLoader,
useVIntl,
} from '@modrinth/ui'
import { computed } from 'vue'
import { useLibrary } from '@/components/ui/library/use-library'
const { filters, instances } = useLibrary()
const { formatMessage } = useVIntl()
const filterCategories = computed<DropdownFilterBarCategory[]>(() => [
{
key: 'instanceType',
label: 'Instance type',
options: [
{ value: 'modpack', label: 'Modpack' },
{ value: 'server', label: 'Server' },
{ value: 'custom', label: 'Custom' },
],
},
{
key: 'gameVersion',
label: 'Game version',
searchable: true,
options: [...new Set(instances.value.map((instance) => instance.game_version))]
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
.map((version) => ({ value: version, label: version })),
},
{
key: 'loader',
label: 'Loader',
options: [...new Set(instances.value.map((instance) => instance.loader))]
.map((loader) => ({
value: loader,
label: formatLoader(formatMessage, loader),
}))
.sort((a, b) => a.label.localeCompare(b.label)),
},
])
</script>
<template>
<DropdownFilterBar
v-model="filters"
:categories="filterCategories"
use-filter-icon
label="Filter by"
add-label="Add filter"
clear-label="Clear filters"
apply-immediately
checkbox-position="right"
/>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { PlusIcon, SearchIcon, SquarePlusIcon } from '@modrinth/assets'
import { ButtonStyled, StyledInput } from '@modrinth/ui'
import { computed, inject } from 'vue'
import FilterMenu from '@/components/ui/library/library-toolbar/filter-menu.vue'
import NewGroupModal from '@/components/ui/library/library-toolbar/new-group-modal.vue'
import SortMenu from '@/components/ui/library/library-toolbar/sort-menu.vue'
import { useLibrary } from '@/components/ui/library/use-library'
const { search, selectedLibraryInstances, openNewGroupModal } = useLibrary()
const showCreationModal = inject<() => void>('showCreationModal')
const selectedInstanceIds = computed(
() =>
new Set([...selectedLibraryInstances.value.values()].map((selection) => selection.instanceId)),
)
function openNewGroup() {
openNewGroupModal(selectedInstanceIds.value)
}
</script>
<template>
<div class="flex flex-col gap-2">
<div class="flex flex-wrap gap-2">
<StyledInput
v-model="search"
:icon="SearchIcon"
type="text"
placeholder="Search"
clearable
wrapper-class="min-w-[16rem] flex-1"
/>
<ButtonStyled>
<button type="button" @click="openNewGroup">
<SquarePlusIcon />
New group
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button type="button" @click="showCreationModal?.()">
<PlusIcon />
New instance
</button>
</ButtonStyled>
</div>
<div class="flex flex-wrap items-center gap-2">
<SortMenu />
<div class="mx-2 h-6 w-px bg-surface-5" />
<FilterMenu />
</div>
</div>
<NewGroupModal />
</template>
@@ -0,0 +1,140 @@
<template>
<NewModal
ref="modal"
no-padding
scrollable
actions-divider
max-width="560px"
width="560px"
:on-hide="closeNewGroupModal"
>
<template #title>
<span class="text-2xl font-semibold text-contrast">Create group</span>
</template>
<div class="flex flex-col gap-2.5 p-6">
<label for="new-group-name" class="font-semibold text-contrast">Group name</label>
<StyledInput
id="new-group-name"
ref="groupNameInput"
v-model="newGroupName"
placeholder="Enter group name"
:maxlength="128"
@click="groupNameInput?.select()"
/>
</div>
<div class="h-px bg-divider" />
<div class="flex h-[400px] flex-col gap-3 overflow-y-auto bg-surface-2 py-4">
<div class="px-6">
<StyledInput
v-model="newGroupSearch"
:icon="SearchIcon"
placeholder="Search instance"
class="w-full"
/>
</div>
<div
v-if="newGroupInstances.length === 0"
class="flex items-center justify-center py-12 text-secondary"
>
No instances found
</div>
<div v-else class="flex flex-col gap-1">
<div
v-for="instance in newGroupInstances"
:key="instance.id"
class="flex items-center justify-between gap-4 px-6 py-1.5 hover:bg-surface-3"
:class="{ 'opacity-60': selectedNewGroupInstanceIds.has(instance.id) }"
>
<div class="flex min-w-0 items-center gap-2.5">
<Avatar
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
:tint-by="instance.id"
:alt="instance.name"
size="2rem"
rounded="md"
/>
<div class="flex min-w-0 items-center gap-2">
<span class="truncate font-semibold text-contrast">{{ instance.name }}</span>
<TagItem v-if="instance.group_ids[0]" class="shrink-0">
{{ groupNamesById.get(instance.group_ids[0]) ?? 'Unknown group' }}
</TagItem>
</div>
</div>
<ButtonStyled
:type="selectedNewGroupInstanceIds.has(instance.id) ? 'outlined' : 'standard'"
>
<button @click="toggleNewGroupInstance(instance.id)">
<CheckIcon v-if="selectedNewGroupInstanceIds.has(instance.id)" />
{{ selectedNewGroupInstanceIds.has(instance.id) ? 'Added' : 'Add' }}
</button>
</ButtonStyled>
</div>
</div>
</div>
<template #actions>
<div class="flex items-center justify-end gap-2">
<ButtonStyled type="outlined">
<button @click="modal?.hide()">
<XIcon />
Cancel
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button :disabled="!canCreateGroup" @click="handleCreateGroup">
<SpinnerIcon v-if="creatingGroup" class="animate-spin" />
<PlusIcon v-else />
Create group
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import { CheckIcon, PlusIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import { Avatar, ButtonStyled, NewModal, StyledInput, TagItem } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, ref, watch } from 'vue'
import { useLibrary } from '@/components/ui/library/use-library'
const {
isNewGroupModalOpen,
libraryGroups,
newGroupName,
newGroupSearch,
selectedNewGroupInstanceIds,
creatingGroup,
newGroupInstances,
canCreateGroup,
closeNewGroupModal,
toggleNewGroupInstance,
createGroup,
clearLibraryInstanceSelection,
} = useLibrary()
const modal = ref<InstanceType<typeof NewModal>>()
const groupNameInput = ref<InstanceType<typeof StyledInput>>()
const groupNamesById = computed(
() => new Map(libraryGroups.value.map((group) => [group.id, group.name])),
)
watch(isNewGroupModalOpen, (open) => {
if (open) {
modal.value?.show()
}
})
async function handleCreateGroup() {
if (await createGroup()) {
clearLibraryInstanceSelection()
modal.value?.hide()
}
}
</script>
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { ArrowUpDownIcon, LayoutGridIcon } from '@modrinth/assets'
import { Combobox, type ComboboxOption } from '@modrinth/ui'
import {
type LibraryGroupBy,
libraryGroupOptions,
type LibrarySort,
librarySortOptions,
useLibrary,
} from '@/components/ui/library/use-library'
const { displayState } = useLibrary()
const sortOptions: ComboboxOption<LibrarySort>[] = librarySortOptions.map((option) => ({
value: option,
label: option,
}))
const groupOptions: ComboboxOption<LibraryGroupBy>[] = [...libraryGroupOptions]
</script>
<template>
<Combobox
v-model="displayState.sortBy"
class="w-max"
:options="sortOptions"
:show-icon-in-selected="false"
dropdown-min-width="160px"
>
<template #prefix>
<ArrowUpDownIcon class="size-5 text-primary" aria-label="Sort by" />
</template>
<template #selected="{ label }">
<span>{{ label }}</span>
</template>
</Combobox>
<Combobox
v-model="displayState.group"
class="w-max"
:options="groupOptions"
:show-icon-in-selected="false"
>
<template #prefix>
<LayoutGridIcon class="size-5 text-primary" aria-label="Group by" />
</template>
<template #selected="{ label }">
<span>{{ label }}</span>
</template>
</Combobox>
</template>
@@ -0,0 +1,904 @@
import type { Labrinth } from '@modrinth/api-client'
import { formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
import { useEventListener, useStorage } from '@vueuse/core'
import dayjs from 'dayjs'
import {
computed,
inject,
type InjectionKey,
provide,
type Ref,
ref,
watch,
watchEffect,
} from 'vue'
import { get_project_v3_many } from '@/helpers/cache.js'
import { toError } from '@/helpers/errors'
import { install_duplicate_instance } from '@/helpers/install'
import { edit, remove } from '@/helpers/instance'
import {
create_group as createInstanceGroup,
delete_group as deleteInstanceGroup,
type InstanceGroupDefinition,
list_groups as listInstanceGroups,
rename_group as renameInstanceGroup,
} from '@/helpers/instance-groups'
import type { GameInstance } from '@/helpers/types'
export const librarySortOptions = [
'Manual',
'Name',
'Last played',
'Hours played',
'Date created',
'Date modified',
] as const
export const libraryGroupOptions = [
{ value: 'Group', label: 'Custom group' },
{ value: 'Loader', label: 'Loader' },
{ value: 'Game version', label: 'Game version' },
{ value: 'None', label: 'No grouping' },
] as const
export type LibrarySort = (typeof librarySortOptions)[number]
export type LibraryGroupBy = (typeof libraryGroupOptions)[number]['value']
export type LibraryFilters = Record<'instanceType' | 'gameVersion' | 'loader', string[]>
export type InstanceGroup = {
id: string
key: string
instances: GameInstance[]
}
export type LibraryInstanceSelection = {
instanceId: string
groupId: string
}
export type ActiveInstanceGroupDrag = {
instances: LibraryInstanceSelection[]
primaryInstanceId: string
fromGroup: string | null
}
export const getLibraryInstanceSelectionKey = ({ instanceId, groupId }: LibraryInstanceSelection) =>
JSON.stringify([groupId, instanceId])
export type InstanceCard = {
instance: GameInstance
playing: boolean
play: (event: MouseEvent | null, context: string) => Promise<void>
stop: (event: MouseEvent | null, context: string) => Promise<void>
addContent: () => Promise<void>
seeInstance: () => Promise<void>
openFolder: () => Promise<void>
}
type InstanceContextMenu = {
showMenu: (event: MouseEvent, item: InstanceCard, options: unknown[]) => void
}
type ConfirmDeleteModal = {
show: () => void
}
type ContextMenuSelection = {
option: string
item: InstanceCard
}
function createLibraryState(instances: Ref<GameInstance[]>) {
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const search = ref('')
const filters = ref<LibraryFilters>({
instanceType: [],
gameVersion: [],
loader: [],
})
const serverProjectIds = ref(new Set<string>())
const libraryGroups = ref<InstanceGroupDefinition[]>([])
const isNewGroupModalOpen = ref(false)
const newGroupName = ref('')
const newGroupSearch = ref('')
const selectedNewGroupInstanceIds = ref(new Set<string>())
const selectedLibraryInstances = ref(new Map<string, LibraryInstanceSelection>())
const isLibraryInstanceSelectionActive = computed(() => selectedLibraryInstances.value.size > 0)
const creatingGroup = ref(false)
const groupIdPendingNameEdit = ref<string | null>(null)
const activeInstanceGroupDrag = ref<ActiveInstanceGroupDrag | null>(null)
const activeDraggedInstanceKeys = computed(
() =>
new Set(activeInstanceGroupDrag.value?.instances.map(getLibraryInstanceSelectionKey) ?? []),
)
const instanceGroupDragTarget = ref<string | null>(null)
const instanceGroupDragPointer = ref({ x: 0, y: 0 })
const isAddingInstanceToGroup = ref(false)
const instanceOptions = ref<InstanceContextMenu | null>(null)
const currentDeleteInstanceId = ref<string | null>(null)
const currentContextGroupId = ref<string | null>(null)
const confirmDeleteModal = ref<ConfirmDeleteModal | null>(null)
const displayState = useStorage<{
group: LibraryGroupBy
sortBy: LibrarySort
collapsedGroups: string[]
}>(
'Instances-grid-display-state',
{
group: 'Group',
sortBy: 'Name',
collapsedGroups: [],
},
localStorage,
{ mergeDefaults: true },
)
if (!librarySortOptions.includes(displayState.value.sortBy)) {
displayState.value.sortBy = 'Name'
}
const linkedInstances = computed(() => instances.value.filter((instance) => instance.link))
const collapsedSectionKeys = computed(() => new Set(displayState.value.collapsedGroups))
const groupNames = computed(
() =>
new Set(
libraryGroups.value
.map((group) => group.name)
.map((group) => group.trim())
.filter((group) => group && group.toLowerCase() !== 'none'),
),
)
const existingGroupNames = computed(
() => new Set(['none', ...Array.from(groupNames.value, (group) => group.toLowerCase())]),
)
const normalizedNewGroupName = computed(() => newGroupName.value.trim().substring(0, 128))
const newGroupInstances = computed(() => {
const query = newGroupSearch.value.trim().toLowerCase()
return instances.value
.filter((instance) => !query || instance.name.toLowerCase().includes(query))
.slice()
.sort((a, b) => {
const groupedDifference = Number(a.group_ids.length > 0) - Number(b.group_ids.length > 0)
if (groupedDifference !== 0) return groupedDifference
return a.name.localeCompare(b.name)
})
})
const canCreateGroup = computed(
() => normalizedNewGroupName.value.length > 0 && !creatingGroup.value,
)
const refreshGroups = async () => {
try {
libraryGroups.value = await listInstanceGroups()
} catch (error) {
handleError(toError(error))
}
}
void refreshGroups()
watchEffect(async () => {
const projectIds = [
...new Set(
linkedInstances.value.flatMap((instance) =>
instance.link?.project_id ? [instance.link.project_id] : [],
),
),
]
if (projectIds.length === 0) {
serverProjectIds.value = new Set()
return
}
try {
const projects = (await get_project_v3_many(
projectIds,
'must_revalidate',
)) as Array<Labrinth.Projects.v3.Project | null>
serverProjectIds.value = new Set(
projects
.filter(
(project): project is Labrinth.Projects.v3.Project => project?.minecraft_server != null,
)
.map((project) => project.id),
)
} catch {
serverProjectIds.value = new Set()
}
})
const getInstanceType = (instance: GameInstance) => {
if (!instance.link) return 'custom'
if (serverProjectIds.value.has(instance.link.project_id ?? '')) return 'server'
return 'modpack'
}
const filteredInstances = computed(() =>
instances.value.filter((instance) => {
const instanceTypeMatches =
filters.value.instanceType.length === 0 ||
filters.value.instanceType.includes(getInstanceType(instance))
const gameVersionMatches =
filters.value.gameVersion.length === 0 ||
filters.value.gameVersion.includes(instance.game_version)
const loaderMatches =
filters.value.loader.length === 0 || filters.value.loader.includes(instance.loader)
return instanceTypeMatches && gameVersionMatches && loaderMatches
}),
)
const instanceGroups = computed<InstanceGroup[]>(() => {
const visibleInstances = filteredInstances.value.filter((instance) =>
instance.name.toLowerCase().includes(search.value.toLowerCase()),
)
switch (displayState.value.sortBy) {
case 'Manual':
break
case 'Name':
visibleInstances.sort((a, b) => a.name.localeCompare(b.name))
break
case 'Last played':
visibleInstances.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
break
case 'Hours played':
visibleInstances.sort(
(a, b) =>
b.recent_time_played +
b.submitted_time_played -
(a.recent_time_played + a.submitted_time_played),
)
break
case 'Date created':
visibleInstances.sort((a, b) => dayjs(b.created).diff(dayjs(a.created)))
break
case 'Date modified':
visibleInstances.sort((a, b) => dayjs(b.modified).diff(dayjs(a.modified)))
break
}
const groupedInstances = new Map<string, { name: string; instances: GameInstance[] }>()
const addToGroup = (id: string, name: string, instance: GameInstance) => {
const group = groupedInstances.get(id) ?? { name, instances: [] }
group.instances.push(instance)
groupedInstances.set(id, group)
}
const groupsById = new Map(libraryGroups.value.map((group) => [group.id, group]))
for (const instance of visibleInstances) {
switch (displayState.value.group) {
case 'Loader':
{
const name = formatLoader(formatMessage, instance.loader)
addToGroup(`Loader:${name}`, name, instance)
}
break
case 'Game version':
addToGroup(`Game version:${instance.game_version}`, instance.game_version, instance)
break
case 'Group':
if (instance.group_ids.length === 0) {
addToGroup('group:none', 'None', instance)
} else {
for (const groupId of instance.group_ids) {
const group = groupsById.get(groupId)
addToGroup(groupId, group?.name ?? groupId, instance)
}
}
break
case 'None':
addToGroup('None:None', 'None', instance)
break
}
}
if (displayState.value.group === 'Group') {
if (!groupedInstances.has('group:none')) {
groupedInstances.set('group:none', { name: 'None', instances: [] })
}
for (const group of libraryGroups.value) {
if (!groupedInstances.has(group.id)) {
groupedInstances.set(group.id, { name: group.name, instances: [] })
}
}
}
const groups = Array.from(groupedInstances, ([id, group]) => ({
id,
key: group.name,
instances: group.instances,
}))
if (displayState.value.sortBy === 'Name') {
groups.sort((a, b) => a.key.localeCompare(b.key) || a.id.localeCompare(b.id))
}
if (displayState.value.group === 'Game version') {
groups.sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }))
}
if (displayState.value.group === 'Group') {
groups.sort((a, b) => {
if (a.key === 'None') return 1
if (b.key === 'None') return -1
return a.key.localeCompare(b.key) || a.id.localeCompare(b.id)
})
}
return groups
})
const getSectionKey = (sectionId: string) => `${displayState.value.group}:${sectionId}`
const isSectionCollapsed = (sectionId: string) =>
collapsedSectionKeys.value.has(getSectionKey(sectionId))
const setSectionCollapsed = (sectionId: string, collapsed: boolean) => {
const sectionKey = getSectionKey(sectionId)
const collapsedSections = new Set(displayState.value.collapsedGroups)
if (collapsed) {
collapsedSections.add(sectionKey)
} else {
collapsedSections.delete(sectionKey)
}
displayState.value.collapsedGroups = [...collapsedSections]
}
const normalizeInstanceGroupId = (groupId: string) => (groupId === 'group:none' ? null : groupId)
const updateInstanceGroupDrag = (
pointer?: { x: number; y: number },
altKey = isAddingInstanceToGroup.value,
) => {
if (pointer) {
instanceGroupDragPointer.value = {
x: pointer.x,
y: pointer.y,
}
}
isAddingInstanceToGroup.value = altKey
}
const startInstanceGroupDrag = (
instanceId: string,
groupId: string,
pointer?: { x: number; y: number },
altKey = false,
) => {
if (displayState.value.group !== 'Group') return
const primaryInstance = { instanceId, groupId }
const primaryInstanceKey = getLibraryInstanceSelectionKey(primaryInstance)
const draggedInstances = selectedLibraryInstances.value.has(primaryInstanceKey)
? [
primaryInstance,
...[...selectedLibraryInstances.value.values()].filter(
(selectedInstance) =>
getLibraryInstanceSelectionKey(selectedInstance) !== primaryInstanceKey,
),
].filter((selectedInstance) =>
instances.value.some((instance) => instance.id === selectedInstance.instanceId),
)
: [primaryInstance]
activeInstanceGroupDrag.value = {
instances: draggedInstances,
primaryInstanceId: instanceId,
fromGroup: normalizeInstanceGroupId(groupId),
}
updateInstanceGroupDrag(pointer, altKey)
}
const finishInstanceGroupDrag = () => {
activeInstanceGroupDrag.value = null
instanceGroupDragTarget.value = null
isAddingInstanceToGroup.value = false
}
const setInstanceGroupDragTarget = (groupId: string | null) => {
instanceGroupDragTarget.value = groupId
}
const getInstanceGroupDropState = (groupId: string) => {
const drag = activeInstanceGroupDrag.value
const draggedSelectionsByInstanceId = new Map<string, LibraryInstanceSelection[]>()
for (const selection of drag?.instances ?? []) {
const selections = draggedSelectionsByInstanceId.get(selection.instanceId) ?? []
selections.push(selection)
draggedSelectionsByInstanceId.set(selection.instanceId, selections)
}
const draggedInstanceIds = new Set(
drag?.instances.map((selection) => selection.instanceId) ?? [],
)
const draggedInstances = instances.value.filter((instance) =>
draggedInstanceIds.has(instance.id),
)
const toGroup = normalizeInstanceGroupId(groupId)
const operation = isAddingInstanceToGroup.value ? 'add' : 'move'
const canAddToGroup = operation !== 'add' || toGroup !== null
const hasChanges = draggedInstances.some((instance) => {
if (operation === 'add') {
return toGroup !== null && !instance.group_ids.includes(toGroup)
}
if (toGroup !== null) {
return !instance.group_ids.includes(toGroup)
}
const sourceGroupIds = new Set(
(draggedSelectionsByInstanceId.get(instance.id) ?? [])
.map((selection) => normalizeInstanceGroupId(selection.groupId))
.filter((sourceGroupId): sourceGroupId is string => sourceGroupId !== null),
)
return instance.group_ids.some((sourceGroupId) => sourceGroupIds.has(sourceGroupId))
})
return {
alreadyInGroup: draggedInstances.length > 0 && !hasChanges,
canDrop: !!drag && draggedInstances.length > 0 && hasChanges && canAddToGroup,
operation,
} as const
}
const instanceGroupDragStatus = computed(() => {
const target = instanceGroupDragTarget.value
if (target) {
const dropState = getInstanceGroupDropState(target)
const isSourceGroup =
normalizeInstanceGroupId(target) === activeInstanceGroupDrag.value?.fromGroup
if (!isSourceGroup && !dropState.canDrop && dropState.alreadyInGroup) {
return 'Already in this group'
}
if (!dropState.canDrop || dropState.operation !== 'add') return
const count = new Set(
activeInstanceGroupDrag.value?.instances.map((selection) => selection.instanceId) ?? [],
).size
return count > 1 ? `Duplicate ${count} instances to group` : 'Duplicate into group'
}
if (isAddingInstanceToGroup.value) return 'Duplicate to group'
return
})
const moveDraggedInstancesToGroup = async (groupId: string, addToGroup = false) => {
const drag = activeInstanceGroupDrag.value
const toGroup = normalizeInstanceGroupId(groupId)
if (!drag) return false
const dropState = getInstanceGroupDropState(groupId)
if (!dropState.canDrop) return false
const shouldAdd = addToGroup && toGroup !== null
const draggedSelectionsByInstanceId = new Map<string, LibraryInstanceSelection[]>()
for (const selection of drag.instances) {
const selections = draggedSelectionsByInstanceId.get(selection.instanceId) ?? []
selections.push(selection)
draggedSelectionsByInstanceId.set(selection.instanceId, selections)
}
const draggedInstanceIds = new Set(draggedSelectionsByInstanceId.keys())
const draggedInstances = instances.value.filter((instance) =>
draggedInstanceIds.has(instance.id),
)
const operations = draggedInstances.flatMap((instance) => {
if (toGroup && instance.group_ids.includes(toGroup)) {
return []
}
const selections = draggedSelectionsByInstanceId.get(instance.id) ?? []
let nextGroupIds = [...instance.group_ids]
if (shouldAdd && toGroup) {
nextGroupIds.push(toGroup)
} else {
const sourceGroupIds = new Set(
selections
.map((selection) => normalizeInstanceGroupId(selection.groupId))
.filter((sourceGroupId): sourceGroupId is string => sourceGroupId !== null),
)
nextGroupIds = nextGroupIds.filter((sourceGroupId) => !sourceGroupIds.has(sourceGroupId))
if (toGroup) {
nextGroupIds.push(toGroup)
}
}
return [
{
instanceId: instance.id,
selections,
destinationSelectionGroupId:
toGroup !== null || nextGroupIds.length === 0 ? groupId : null,
promise: edit(instance.id, { group_ids: nextGroupIds }),
},
]
})
const results = await Promise.allSettled(operations.map((operation) => operation.promise))
let movedInstanceCount = 0
const nextSelectedInstances = new Map(selectedLibraryInstances.value)
for (const [index, result] of results.entries()) {
if (result.status === 'rejected') {
handleError(toError(result.reason))
} else {
movedInstanceCount++
const operation = operations[index]
const selectedMovedSelections = operation.selections.filter((selection) =>
nextSelectedInstances.has(getLibraryInstanceSelectionKey(selection)),
)
for (const selection of selectedMovedSelections) {
nextSelectedInstances.delete(getLibraryInstanceSelectionKey(selection))
}
if (selectedMovedSelections.length > 0 && operation.destinationSelectionGroupId !== null) {
const movedSelection = {
instanceId: operation.instanceId,
groupId: operation.destinationSelectionGroupId,
}
nextSelectedInstances.set(getLibraryInstanceSelectionKey(movedSelection), movedSelection)
}
}
}
selectedLibraryInstances.value = nextSelectedInstances
return movedInstanceCount > 0
}
useEventListener(window, 'keydown', (event) => {
if (event.key === 'Alt' && activeInstanceGroupDrag.value) {
isAddingInstanceToGroup.value = true
}
})
useEventListener(window, 'keyup', (event) => {
if (event.key === 'Alt') {
isAddingInstanceToGroup.value = false
}
})
useEventListener(window, 'blur', () => {
isAddingInstanceToGroup.value = false
})
const getDefaultNewGroupName = () => {
let groupNumber = groupNames.value.size + 1
while (existingGroupNames.value.has(`group ${groupNumber}`.toLowerCase())) {
groupNumber++
}
return `Group ${groupNumber}`
}
const openNewGroupModal = (instanceIds: Iterable<string> = []) => {
newGroupName.value = getDefaultNewGroupName()
newGroupSearch.value = ''
selectedNewGroupInstanceIds.value = new Set(instanceIds)
isNewGroupModalOpen.value = true
}
const closeNewGroupModal = () => {
isNewGroupModalOpen.value = false
}
const toggleNewGroupInstance = (instanceId: string) => {
const selectedIds = new Set(selectedNewGroupInstanceIds.value)
if (selectedIds.has(instanceId)) {
selectedIds.delete(instanceId)
} else {
selectedIds.add(instanceId)
}
selectedNewGroupInstanceIds.value = selectedIds
}
const clearLibraryInstanceSelection = () => {
selectedLibraryInstances.value = new Map()
}
watch(() => displayState.value.group, clearLibraryInstanceSelection)
const setSelectedLibraryInstances = (selections: Iterable<LibraryInstanceSelection>) => {
selectedLibraryInstances.value = new Map(
[...selections].map((selection) => [getLibraryInstanceSelectionKey(selection), selection]),
)
}
const toggleLibraryInstanceSelection = (selection: LibraryInstanceSelection) => {
const selectedInstances = new Map(selectedLibraryInstances.value)
const selectionKey = getLibraryInstanceSelectionKey(selection)
if (selectedInstances.has(selectionKey)) {
selectedInstances.delete(selectionKey)
} else {
selectedInstances.set(selectionKey, selection)
}
selectedLibraryInstances.value = selectedInstances
}
useEventListener(window, 'keydown', (event) => {
if (
event.key === 'Escape' &&
!event.defaultPrevented &&
isLibraryInstanceSelectionActive.value
) {
clearLibraryInstanceSelection()
}
})
const createGroup = async () => {
if (!canCreateGroup.value) return false
creatingGroup.value = true
try {
const group = await createInstanceGroup(normalizedNewGroupName.value)
libraryGroups.value = [
...libraryGroups.value.filter((existingGroup) => existingGroup.id !== group.id),
group,
]
await Promise.all(
instances.value
.filter((instance) => selectedNewGroupInstanceIds.value.has(instance.id))
.map((instance) =>
edit(instance.id, {
group_ids: [...instance.group_ids, group.id],
}),
),
)
return true
} catch (error) {
handleError(toError(error))
await refreshGroups()
return false
} finally {
creatingGroup.value = false
}
}
const createDefaultGroup = async (instanceIds: Iterable<string> = []) => {
if (creatingGroup.value) return false
creatingGroup.value = true
try {
const group = await createInstanceGroup(getDefaultNewGroupName())
libraryGroups.value = [
...libraryGroups.value.filter((existingGroup) => existingGroup.id !== group.id),
group,
]
const selectedInstanceIds = new Set(instanceIds)
const instancesToAdd = instances.value.filter((instance) =>
selectedInstanceIds.has(instance.id),
)
await Promise.all(
instancesToAdd.map((instance) =>
edit(instance.id, {
group_ids: [...instance.group_ids, group.id],
}),
),
)
if (instancesToAdd.length > 0) {
setSelectedLibraryInstances(
instancesToAdd.map((instance) => ({
instanceId: instance.id,
groupId: group.id,
})),
)
}
displayState.value.group = 'Group'
groupIdPendingNameEdit.value = group.id
return true
} catch (error) {
handleError(toError(error))
await refreshGroups()
return false
} finally {
creatingGroup.value = false
}
}
const completePendingGroupNameEdit = (groupId: string) => {
if (groupIdPendingNameEdit.value === groupId) {
groupIdPendingNameEdit.value = null
}
}
const deleteGroup = async (groupId: string) => {
try {
await deleteInstanceGroup(groupId)
libraryGroups.value = libraryGroups.value.filter((group) => group.id !== groupId)
return true
} catch (error) {
handleError(toError(error))
await refreshGroups()
return false
}
}
const renameGroup = async (groupId: string, newName: string) => {
const normalizedNewName = newName.trim()
const currentGroup = libraryGroups.value.find((group) => group.id === groupId)
if (currentGroup?.name === normalizedNewName) return true
try {
const renamedGroup = await renameInstanceGroup(groupId, normalizedNewName)
libraryGroups.value = [
...libraryGroups.value.filter((group) => group.id !== groupId),
renamedGroup,
]
return true
} catch (error) {
handleError(toError(error))
await refreshGroups()
return false
}
}
const deleteInstance = async () => {
if (!currentDeleteInstanceId.value) return
await remove(currentDeleteInstanceId.value).catch((error) => handleError(toError(error)))
currentDeleteInstanceId.value = null
}
const duplicateInstance = async (instanceId: string) => {
await install_duplicate_instance(instanceId).catch((error) => handleError(toError(error)))
}
const handleInstanceContextMenu = (
event: MouseEvent,
item: InstanceCard,
instanceGroupId: string,
) => {
currentContextGroupId.value =
displayState.value.group === 'Group' && instanceGroupId !== 'group:none'
? instanceGroupId
: null
const baseOptions = [
...(item.instance.quarantined ? [] : [{ name: 'add_content' }, { type: 'divider' }]),
{ name: 'edit' },
{ name: 'duplicate' },
{ name: 'open' },
{ name: 'copy' },
...(currentContextGroupId.value
? [{ name: 'remove_from_group' }, { type: 'divider' }]
: [{ type: 'divider' }]),
{ name: 'delete', color: 'danger' },
]
instanceOptions.value?.showMenu(
event,
item,
item.playing
? [{ name: 'stop', color: 'danger' }, ...baseOptions]
: [
...(item.instance.quarantined ? [] : [{ name: 'play', color: 'primary' }]),
...baseOptions,
],
)
}
const handleInstanceOption = async ({ option, item }: ContextMenuSelection) => {
switch (option) {
case 'play':
await item.play(null, 'InstanceGridContextMenu')
break
case 'stop':
await item.stop(null, 'InstanceGridContextMenu')
break
case 'add_content':
await item.addContent()
break
case 'edit':
await item.seeInstance()
break
case 'duplicate':
if (item.instance.install_stage === 'installed') {
await duplicateInstance(item.instance.id)
}
break
case 'open':
await item.openFolder()
break
case 'copy':
await navigator.clipboard.writeText(item.instance.id)
break
case 'remove_from_group':
if (currentContextGroupId.value) {
const groupId = currentContextGroupId.value
await edit(item.instance.id, {
group_ids: item.instance.group_ids.filter(
(instanceGroupId) => instanceGroupId !== groupId,
),
}).catch((error) => handleError(toError(error)))
}
break
case 'delete':
currentDeleteInstanceId.value = item.instance.id
confirmDeleteModal.value?.show()
break
}
}
return {
instances,
libraryGroups,
search,
filters,
displayState,
instanceGroups,
isNewGroupModalOpen,
newGroupName,
newGroupSearch,
selectedNewGroupInstanceIds,
selectedLibraryInstances,
isLibraryInstanceSelectionActive,
activeInstanceGroupDrag,
activeDraggedInstanceKeys,
instanceGroupDragTarget,
instanceGroupDragPointer,
instanceGroupDragStatus,
isAddingInstanceToGroup,
creatingGroup,
groupIdPendingNameEdit,
newGroupInstances,
canCreateGroup,
instanceOptions,
confirmDeleteModal,
isSectionCollapsed,
setSectionCollapsed,
startInstanceGroupDrag,
updateInstanceGroupDrag,
finishInstanceGroupDrag,
setInstanceGroupDragTarget,
getInstanceGroupDropState,
moveDraggedInstancesToGroup,
openNewGroupModal,
closeNewGroupModal,
toggleNewGroupInstance,
clearLibraryInstanceSelection,
setSelectedLibraryInstances,
toggleLibraryInstanceSelection,
createGroup,
createDefaultGroup,
completePendingGroupNameEdit,
deleteGroup,
renameGroup,
deleteInstance,
handleInstanceContextMenu,
handleInstanceOption,
}
}
export type LibraryState = ReturnType<typeof createLibraryState>
const libraryKey: InjectionKey<LibraryState> = Symbol('library')
export function provideLibrary(instances: Ref<GameInstance[]>) {
const library = createLibraryState(instances)
provide(libraryKey, library)
return library
}
export function useLibrary() {
const library = inject(libraryKey)
if (!library) {
throw new Error('useLibrary must be called within a library provider')
}
return library
}
@@ -1,7 +1,7 @@
<template>
<NewModal ref="modal" :header="formatMessage(messages.header)" fade="danger" max-width="500px">
<NewModal ref="modal" :header="modalHeader" fade="danger" max-width="500px">
<Admonition type="critical" :header="formatMessage(messages.admonitionHeader)">
{{ formatMessage(messages.admonitionBody) }}
{{ admonitionBody }}
</Admonition>
<template #actions>
@@ -15,7 +15,7 @@
<ButtonStyled color="red">
<button @click="confirm">
<TrashIcon />
{{ formatMessage(messages.deleteButton) }}
{{ deleteButtonLabel }}
</button>
</ButtonStyled>
</div>
@@ -33,7 +33,7 @@ import {
NewModal,
useVIntl,
} from '@modrinth/ui'
import { ref } from 'vue'
import { computed, ref } from 'vue'
const { formatMessage } = useVIntl()
@@ -42,6 +42,10 @@ const messages = defineMessages({
id: 'app.instance.confirm-delete.header',
defaultMessage: 'Delete instance',
},
bulkHeader: {
id: 'app.instance.confirm-delete.bulk-header',
defaultMessage: 'Delete {count} instances',
},
admonitionHeader: {
id: 'app.instance.confirm-delete.admonition-header',
defaultMessage: 'This action cannot be undone',
@@ -51,17 +55,43 @@ const messages = defineMessages({
defaultMessage:
'All data for your instance will be permanently deleted, including your worlds, configs, and all installed content.',
},
bulkAdmonitionBody: {
id: 'app.instance.confirm-delete.bulk-admonition-body',
defaultMessage:
'All data for these {count} instances will be permanently deleted, including their worlds, configs, and all installed content.',
},
deleteButton: {
id: 'app.instance.confirm-delete.delete-button',
defaultMessage: 'Delete instance',
},
bulkDeleteButton: {
id: 'app.instance.confirm-delete.bulk-delete-button',
defaultMessage: 'Delete {count} instances',
},
})
const { count = 1 } = defineProps<{
count?: number
}>()
const emit = defineEmits<{
(e: 'delete'): void
}>()
const modal = ref<InstanceType<typeof NewModal>>()
const modalHeader = computed(() =>
count === 1 ? formatMessage(messages.header) : formatMessage(messages.bulkHeader, { count }),
)
const admonitionBody = computed(() =>
count === 1
? formatMessage(messages.admonitionBody)
: formatMessage(messages.bulkAdmonitionBody, { count }),
)
const deleteButtonLabel = computed(() =>
count === 1
? formatMessage(messages.deleteButton)
: formatMessage(messages.bulkDeleteButton, { count }),
)
function show() {
modal.value?.show()
@@ -0,0 +1,130 @@
<script setup lang="ts">
import { CheckIcon, RadioButtonIcon } from '@modrinth/assets'
import { Accordion, defineMessages, useVIntl } from '@modrinth/ui'
import { computed, onUnmounted, ref } from 'vue'
import { injectOnboardingChecklist } from '@/providers/onboarding-checklist'
const emit = defineEmits<{
'create-instance': []
'login-minecraft': []
'login-modrinth': []
}>()
const { formatMessage } = useVIntl()
const {
hasCreatedInstance,
hasLoggedIntoMinecraft,
hasLoggedIntoModrinth,
isReady,
showChecklist,
} = injectOnboardingChecklist()
const collapsedCornersVisible = ref(false)
let collapseTimer: ReturnType<typeof setTimeout> | undefined
const messages = defineMessages({
title: {
id: 'onboarding-checklist.title',
defaultMessage: 'Getting started',
},
createInstance: {
id: 'onboarding-checklist.create-instance',
defaultMessage: 'Create first instance',
},
loginMinecraft: {
id: 'onboarding-checklist.login-minecraft',
defaultMessage: 'Sign in to Minecraft',
},
loginModrinth: {
id: 'onboarding-checklist.login-modrinth',
defaultMessage: 'Sign in to Modrinth',
},
})
const steps = computed(() => [
{
id: 'create-instance',
label: formatMessage(messages.createInstance),
complete: hasCreatedInstance.value,
action: () => emit('create-instance'),
},
{
id: 'login-minecraft',
label: formatMessage(messages.loginMinecraft),
complete: hasLoggedIntoMinecraft.value,
action: () => emit('login-minecraft'),
},
{
id: 'login-modrinth',
label: formatMessage(messages.loginModrinth),
complete: hasLoggedIntoModrinth.value,
action: () => emit('login-modrinth'),
},
])
const accordionButtonClass = computed(
() =>
`flex w-full cursor-pointer items-center justify-between rounded-t-2xl border border-solid border-button-border bg-button-bg p-3 text-left text-contrast transition-[filter] hover:brightness-110${collapsedCornersVisible.value ? ' rounded-b-2xl' : ''}`,
)
function handleAccordionOpen() {
clearTimeout(collapseTimer)
collapsedCornersVisible.value = false
}
function handleAccordionClose() {
clearTimeout(collapseTimer)
collapseTimer = setTimeout(() => {
collapsedCornersVisible.value = true
}, 300)
}
onUnmounted(() => clearTimeout(collapseTimer))
</script>
<template>
<div
v-if="isReady && showChecklist"
class="border-0 border-b-[1px] border-solid border-[--brand-gradient-border] px-3 p-4"
>
<Accordion
open-by-default
class="w-full overflow-hidden rounded-2xl text-base"
:button-class="accordionButtonClass"
content-class="flex flex-col gap-2 rounded-b-2xl border border-t-0 border-solid border-surface-5 bg-button-bg p-3"
@on-open="handleAccordionOpen"
@on-close="handleAccordionClose"
>
<template #title>
<span class="font-semibold leading-6">{{ formatMessage(messages.title) }}</span>
</template>
<button
v-for="step in steps"
:key="step.id"
type="button"
class="flex h-10 w-full items-center gap-2 rounded-xl border border-solid border-button-border bg-button-bg px-4 py-2.5 text-left text-primary shadow-[0_1px_0.5px_rgb(0_0_0_/_12%)] transition-[filter]"
:class="
step.complete
? '!cursor-default opacity-50'
: 'cursor-pointer hover:brightness-110 active:brightness-90'
"
:disabled="step.complete"
@click="step.action"
>
<span
v-if="step.complete"
class="flex size-[18px] items-center justify-center rounded-full bg-primary mr-0.5 relative left-px"
>
<CheckIcon class="size-3 invert [stroke-width:3] top-px" />
</span>
<RadioButtonIcon v-else class="size-5 shrink-0" />
<span
class="min-w-0 truncate font-medium leading-5"
:class="{ 'text-secondary line-through': step.complete }"
>
{{ step.label }}
</span>
</button>
</Accordion>
</div>
</template>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Combobox, defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
import { defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
import { ref, watch } from 'vue'
import { get, set } from '@/helpers/settings.ts'
@@ -57,26 +57,6 @@ const messages = defineMessages({
id: 'app.appearance-settings.minimize-launcher.description',
defaultMessage: 'Minimize the launcher when a Minecraft process starts.',
},
defaultLandingPageTitle: {
id: 'app.appearance-settings.default-landing-page.title',
defaultMessage: 'Default landing page',
},
defaultLandingPageDescription: {
id: 'app.appearance-settings.default-landing-page.description',
defaultMessage: 'Change the page to which the launcher opens on.',
},
defaultLandingPageHome: {
id: 'app.appearance-settings.default-landing-page.home',
defaultMessage: 'Home',
},
defaultLandingPageLibrary: {
id: 'app.appearance-settings.default-landing-page.library',
defaultMessage: 'Library',
},
selectOption: {
id: 'app.appearance-settings.select-option',
defaultMessage: 'Select an option',
},
jumpBackIntoWorldsTitle: {
id: 'app.appearance-settings.jump-back-into-worlds.title',
defaultMessage: 'Jump back into worlds',
@@ -230,32 +210,6 @@ watch(
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.defaultLandingPageTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.defaultLandingPageDescription) }}</p>
</div>
<Combobox
id="opening-page"
v-model="settings.default_page"
name="Opening page dropdown"
class="max-w-40"
:options="[
{
value: 'Home',
label: formatMessage(messages.defaultLandingPageHome),
},
{
value: 'Library',
label: formatMessage(messages.defaultLandingPageLibrary),
},
]"
:display-value="settings.default_page ?? 'Select an option'"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
@@ -1,6 +1,14 @@
<script setup>
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
import {
ButtonStyled,
defineMessages,
injectNotificationManager,
Slider,
StyledInput,
Toggle,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
@@ -11,9 +19,23 @@ import { showAppDbBackupsFolder } from '@/helpers/utils.js'
import { useTheming } from '@/store/state'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const settings = ref(await get())
const purgeCacheConfirmModal = ref(null)
const alwaysShowCopyDetailsFlag = 'always_show_copy_details'
const messages = defineMessages({
alwaysShowCopyDetailsTitle: {
id: 'app.resource-management-settings.always-show-copy-details.title',
defaultMessage: 'Always show copy details',
},
alwaysShowCopyDetailsDescription: {
id: 'app.resource-management-settings.always-show-copy-details.description',
defaultMessage:
'Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs.',
},
})
watch(
settings,
@@ -154,6 +176,28 @@ async function findLauncherDir() {
</p>
</div>
<div class="flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.alwaysShowCopyDetailsTitle) }}
</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.alwaysShowCopyDetailsDescription) }}
</p>
</div>
<Toggle
id="always-show-copy-details"
:model-value="themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
}
"
/>
</div>
<div class="flex flex-col gap-2.5">
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">App database backups</h2>
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
@@ -6,11 +6,9 @@ import {
injectPopupNotificationManager,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import { type Ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { config } from '@/config'
import { get_user } from '@/helpers/cache'
import { toError } from '@/helpers/errors'
import {
@@ -216,7 +214,7 @@ export function useSharedInstanceInviteHandler(
markNotificationRead(notification).catch((error) => handleError(toError(error))),
onOpenActor: () => {
if (invite.invitedByUsername) {
openUrl(`${config.siteUrl}/user/${encodeURIComponent(invite.invitedByUsername)}`)
void router.push(`/user/${encodeURIComponent(invite.invitedByUsername)}`)
}
},
},
@@ -260,7 +260,9 @@ onUnmounted(() => {
<template>
<div v-if="loading" class="flex flex-col gap-2">
<span class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold">
<span
class="flex mt-1 mb-3 leading-none items-center gap-1 text-2xl font-semibold text-contrast"
>
Jump back in
</span>
<div class="text-center py-4">
@@ -273,7 +275,7 @@ onUnmounted(() => {
</HeadingLink>
<span
v-else
class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold"
class="flex mt-1 mb-3 leading-none items-center gap-1 text-2xl font-semibold text-contrast"
>
Jump back in
</span>
@@ -23,6 +23,7 @@ import {
type InstallProgress,
} from '@/helpers/install'
import { get_many as getInstances } from '@/helpers/instance'
import { useTheming } from '@/store/state'
const messages = defineMessages({
installs: {
@@ -221,13 +222,6 @@ const failureSummaryMessages = defineMessages({
})
const visibleJobStatuses = new Set<InstallJobStatus>(['queued', 'running', 'failed', 'interrupted'])
const copyDetailsStallMs = 30_000
interface ProgressSnapshot {
signature: string
changedAt: number
timeout: number | null
}
function getDisplayIconUrl(icon: string | null | undefined): string | null {
if (!icon) return null
@@ -241,6 +235,7 @@ export async function useInstallJobNotifications(opts: {
onChange: () => void
}) {
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const jobs = ref<InstallJobSnapshot[]>([])
const iconUrls = ref<Record<string, string | null>>({})
const instanceNames = ref<Record<string, string>>({})
@@ -250,7 +245,6 @@ export async function useInstallJobNotifications(opts: {
let metadataRequest = 0
let nextJobOrder = 0
const copiedResetTimeouts = new Map<string, number>()
const progressSnapshots = new Map<string, ProgressSnapshot>()
function getTitle(job: InstallJobSnapshot): string {
if (job.display?.title) return job.display.title
@@ -421,116 +415,14 @@ export async function useInstallJobNotifications(opts: {
return job.status === 'failed' || job.status === 'interrupted'
}
function canShowStalledProgressDetails(job: InstallJobSnapshot): boolean {
return (
job.status === 'running' &&
job.phase !== 'preparing_instance' &&
job.phase !== 'finalizing' &&
job.phase !== 'rolling_back'
)
}
function getJobSortRank(job: InstallJobSnapshot): number {
if (isTerminalJob(job)) return 0
if (job.status === 'queued' || job.phase === 'preparing_instance') return 2
return 1
}
function progressSignature(job: InstallJobSnapshot): string {
const progress = job.progress
const secondary = progress?.secondary
return [
job.status,
job.phase,
JSON.stringify(job.details),
progress?.current ?? '',
progress?.total ?? '',
secondary?.current ?? '',
secondary?.total ?? '',
].join(':')
}
function clearCopied(jobId: string) {
if (!copiedJobIds.value.has(jobId)) {
return
}
const timeout = copiedResetTimeouts.get(jobId)
if (timeout != null) {
window.clearTimeout(timeout)
copiedResetTimeouts.delete(jobId)
}
const nextCopiedJobIds = new Set(copiedJobIds.value)
nextCopiedJobIds.delete(jobId)
copiedJobIds.value = nextCopiedJobIds
}
function clearProgressSnapshot(jobId: string) {
const snapshot = progressSnapshots.get(jobId)
if (snapshot?.timeout != null) {
window.clearTimeout(snapshot.timeout)
}
progressSnapshots.delete(jobId)
}
function scheduleStaleProgressRefresh(jobId: string) {
const snapshot = progressSnapshots.get(jobId)
if (!snapshot) {
return
}
snapshot.timeout = window.setTimeout(() => {
const snapshot = progressSnapshots.get(jobId)
if (!snapshot) {
return
}
snapshot.timeout = null
opts.onChange()
}, copyDetailsStallMs)
}
function syncProgressSnapshots(nextJobs: InstallJobSnapshot[]) {
const trackedJobIds = new Set<string>()
const now = Date.now()
for (const job of nextJobs) {
if (!canShowStalledProgressDetails(job)) {
continue
}
trackedJobIds.add(job.job_id)
const signature = progressSignature(job)
const snapshot = progressSnapshots.get(job.job_id)
if (snapshot?.signature === signature) {
continue
}
clearProgressSnapshot(job.job_id)
clearCopied(job.job_id)
progressSnapshots.set(job.job_id, {
signature,
changedAt: now,
timeout: null,
})
scheduleStaleProgressRefresh(job.job_id)
}
for (const jobId of progressSnapshots.keys()) {
if (!trackedJobIds.has(jobId)) {
clearProgressSnapshot(jobId)
}
}
}
function hasStalledProgress(job: InstallJobSnapshot): boolean {
const snapshot = progressSnapshots.get(job.job_id)
return !!snapshot && Date.now() - snapshot.changedAt >= copyDetailsStallMs
}
function shouldShowCopyDetails(job: InstallJobSnapshot): boolean {
return isTerminalJob(job) || (canShowStalledProgressDetails(job) && hasStalledProgress(job))
return isTerminalJob(job) || themeStore.getFeatureFlag('always_show_copy_details')
}
function isCopied(job: InstallJobSnapshot): boolean {
@@ -607,6 +499,16 @@ export async function useInstallJobNotifications(opts: {
return buttons
}
function getDismissHandler(job: InstallJobSnapshot): (() => Promise<void>) | undefined {
if (isTerminalJob(job)) {
return async () => {
await install_job_dismiss(job.job_id).catch(opts.handleError)
await refresh()
}
}
return undefined
}
function setJobs(nextJobs: InstallJobSnapshot[]) {
for (const job of nextJobs) {
if (!jobOrder.has(job.job_id)) {
@@ -615,7 +517,6 @@ export async function useInstallJobNotifications(opts: {
}
const visibleJobs = nextJobs.filter((job) => visibleJobStatuses.has(job.status))
syncProgressSnapshots(visibleJobs)
jobs.value = visibleJobs.sort(
(a, b) =>
@@ -642,12 +543,8 @@ export async function useInstallJobNotifications(opts: {
progressCurrent: isTerminalJob(job) ? undefined : progress?.current,
progressTotal: isTerminalJob(job) ? undefined : progress?.total,
buttons: getButtons(job),
onDismiss: isTerminalJob(job)
? async () => {
await install_job_dismiss(job.job_id).catch(opts.handleError)
await refresh()
}
: undefined,
dismissible: isTerminalJob(job),
onDismiss: getDismissHandler(job),
}
}),
)
@@ -730,8 +627,8 @@ export async function useInstallJobNotifications(opts: {
void refreshMetadata()
}
await refresh(false)
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
await refresh(false)
return {
active: computed(() => jobs.value.length > 0),
@@ -743,9 +640,6 @@ export async function useInstallJobNotifications(opts: {
for (const timeout of copiedResetTimeouts.values()) {
window.clearTimeout(timeout)
}
for (const jobId of progressSnapshots.keys()) {
clearProgressSnapshot(jobId)
}
unlisten()
},
}
+1 -1
View File
@@ -11,7 +11,7 @@ interface ProjectProperties extends InstanceProperties {
}
type AnalyticsEventMap = {
Launched: { version: string; dev: boolean; onboarded: boolean }
Launched: { version: string; dev: boolean }
PageView: { path: string; fromPath: string; failed: unknown }
InstanceCreate: { source: string }
InstanceCreateStart: { source: string }
@@ -0,0 +1,22 @@
import { invoke } from '@tauri-apps/api/core'
export type InstanceGroupDefinition = {
id: string
name: string
}
export async function list_groups(): Promise<InstanceGroupDefinition[]> {
return await invoke('plugin:instance|instance_list_groups')
}
export async function create_group(name: string): Promise<InstanceGroupDefinition> {
return await invoke('plugin:instance|instance_create_group', { name })
}
export async function rename_group(id: string, newName: string): Promise<InstanceGroupDefinition> {
return await invoke('plugin:instance|instance_rename_group', { id, newName })
}
export async function delete_group(id: string): Promise<void> {
return await invoke('plugin:instance|instance_delete_group', { id })
}
@@ -0,0 +1,12 @@
import { invoke } from '@tauri-apps/api/core'
export interface OnboardingChecklist {
has_created_instance: boolean
has_logged_into_minecraft: boolean
has_logged_into_modrinth: boolean
show_checklist: boolean
}
export async function getOnboardingChecklist(): Promise<OnboardingChecklist> {
return await invoke('plugin:onboarding-checklist|get_onboarding_checklist')
}
@@ -48,8 +48,6 @@ export type AppSettings = {
discord_rpc: boolean
personalized_ads: boolean
onboarded: boolean
extra_launch_args: string[]
custom_env_vars: [string, string][]
memory: MemorySettings
+1 -3
View File
@@ -14,7 +14,7 @@ export type GameInstance = {
loader: InstanceLoader
loader_version?: string
groups: string[]
group_ids: string[]
link?: InstanceLink | null
shared_instance?: SharedInstanceAttachment | null
@@ -201,8 +201,6 @@ type AppSettings = {
developer_mode: boolean
personalized_ads: boolean
onboarded: boolean
extra_launch_args: string[]
custom_env_vars: [string, string][]
memory: MemorySettings
+51 -6
View File
@@ -1,11 +1,56 @@
import type { Labrinth } from '@modrinth/api-client'
import { invoke } from '@tauri-apps/api/core'
export type SearchUser = {
id: string
username: string
avatar_url: string | null
// Converts user profile links from rendered Markdown/any dynamic content into app routes.
export function parse_modrinth_user_link(href: string): string | null {
try {
const url = new URL(href)
if (url.hostname !== 'modrinth.com' && url.hostname !== 'www.modrinth.com') return null
const segments = url.pathname.split('/').filter(Boolean)
if (segments[0]?.toLowerCase() !== 'user' || !segments[1] || segments.length > 3) return null
const path = `/user/${encodeURIComponent(decodeURIComponent(segments[1]))}`
return segments[2] ? `${path}/${encodeURIComponent(decodeURIComponent(segments[2]))}` : path
} catch {
return null
}
}
export async function search_user(query: string): Promise<SearchUser[]> {
return await invoke<SearchUser[]>('plugin:users|search_user', { query })
export async function search_user(query: string): Promise<Labrinth.Users.v3.SearchUser[]> {
return await invoke<Labrinth.Users.v3.SearchUser[]>('plugin:users|search_user', { query })
}
export async function get_user_profile(userId: string): Promise<Labrinth.Users.v3.User> {
return await invoke<Labrinth.Users.v3.User>('plugin:users|get_user_profile', { userId })
}
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v2.Project[]> {
return await invoke<Labrinth.Projects.v2.Project[]>('plugin:users|get_user_projects', {
userId,
})
}
export async function get_user_organizations(
userId: string,
): Promise<Labrinth.Organizations.v3.Organization[]> {
return await invoke<Labrinth.Organizations.v3.Organization[]>(
'plugin:users|get_user_organizations',
{ userId },
)
}
export async function get_user_collections(
userId: string,
): Promise<Labrinth.Collections.Collection[]> {
return await invoke<Labrinth.Collections.Collection[]>('plugin:users|get_user_collections', {
userId,
})
}
export async function patch_user(
userId: string,
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
): Promise<void> {
await invoke('plugin:users|patch_user', { userId, patch })
}
@@ -146,18 +146,6 @@
"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": "ألغ تفعيل عرض الاسم فوق لاعبك في صفحة المظاهر."
},
@@ -182,9 +170,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "الزخارف التقليدية"
},
"app.appearance-settings.select-option": {
"message": "حدد خياراً"
},
"app.appearance-settings.show-play-time.description": {
"message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام."
},
@@ -77,18 +77,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Barevný motiv"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Změňte stránku, která se otevře po spuštění launcheru."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Domů"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Knihovna"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Výchozí stránka po spuštění"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Zakáže zobrazení jmenovky nad vaší postavou na stránce skinů."
},
@@ -113,9 +101,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Systémové dekorace"
},
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
},
+255 -15
View File
@@ -29,6 +29,9 @@
"app.action-bar.install.summary.app-closing": {
"message": "Annulleret på grund af appen blev lukket"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "Kunne ikke indlæse modpack"
},
"app.action-bar.install.summary.canceled": {
"message": "Annulleret"
},
@@ -47,9 +50,45 @@
"app.action-bar.install.summary.instance-not-found": {
"message": "Instance kunne ikke blive fundet"
},
"app.action-bar.install.summary.invalid-file-path": {
"message": "Fil sti er ugyldig"
},
"app.action-bar.install.summary.invalid-modpack": {
"message": "Modpack data ugyldig"
},
"app.action-bar.install.summary.invalid-modpack-files": {
"message": "Modpack har ugyldig metadata"
},
"app.action-bar.install.summary.java-setup-failed": {
"message": "Java setup kunne ikke færdiggøres"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "Loader setup fejlede"
},
"app.action-bar.install.summary.local-data-error": {
"message": "Kunne ikke opdatere lokal data"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "Minecraft setup fejlede"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "Kunne ikke få forbindelse til Modrinth"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "Ingen adgang til at skrive"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "Kunne ikke download pack"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "Noget gik galt"
},
"app.action-bar.install.unknown-instance": {
"message": "Ukendt instance"
},
"app.action-bar.install.updating-shared-content": {
"message": "Opdatere delt indhold"
},
"app.action-bar.make-primary-instance": {
"message": "Gør til primære instance"
},
@@ -83,6 +122,15 @@
"app.action-bar.view-logs": {
"message": "Vis logs"
},
"app.ads-consent.accept": {
"message": "Tillad alle"
},
"app.ads-consent.body": {
"message": "Reklamer gør Modrinth muligt og finansiere skaber udbetalinger. Vores partnere må gemme og tilgå cookies i appen for at personliggøre reklamer og måle effektivitet."
},
"app.ads-consent.reject": {
"message": "Afvis alle"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiverer avanceret gengivelse som sløringseffekter, der kan forårsage ydelsesproblemer uden hardwareaccelereret gengivelse."
},
@@ -95,18 +143,6 @@
"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."
},
@@ -125,9 +161,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcher"
},
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.show-play-time.description": {
"message": "Vis hvor meget tid du har brugt på at spille en instance."
},
@@ -224,6 +257,9 @@
"app.install.phase.preparing_java.downloading": {
"message": "Downloader Java {version}"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Tilføjet"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Al' data for din instance vil blive permanent slettet, dette inkludere dine verdener, konfigurationer, og alt installeret indhold."
},
@@ -269,12 +305,51 @@
"app.instance.mods.successfully-uploaded": {
"message": "Uploadet"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "for at tilgå denne side."
},
"app.instance.share.locked.signed-out-heading": {
"message": "Ikke logget ind"
},
"app.instance.share.locked.switch-account-button": {
"message": "Skift konto"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Forkert konto"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Hvad vil der ske?"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}'s avatar"
},
"app.instance.share.sign-in.button": {
"message": "Log ind"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Installer alligevel"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instans"
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Log ind som"
},
"app.instance.worlds.add-server": {
"message": "Tilføj server"
},
"app.instance.worlds.browse-servers": {
"message": "Søg servere"
},
"app.instance.worlds.delete-world-modal.delete-button": {
"message": "Slet verden"
},
"app.instance.worlds.delete-world-modal.title": {
"message": "Slet verden"
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Sletter {name}"
},
"app.instance.worlds.filter-modded": {
"message": "Moddet"
},
@@ -293,18 +368,42 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Ingen servere eller verdener tilføjet"
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Fjern server"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Fjern server"
},
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Fjerner {name}"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Søg {count} verdener..."
},
"app.modal.install-to-play.additional-context": {
"message": "Yderligere kontekst"
},
"app.modal.install-to-play.delete-instance": {
"message": "Slet instans"
},
"app.modal.install-to-play.header": {
"message": "Installer for at spille"
},
"app.modal.install-to-play.install-anyway": {
"message": "Installer alligevel"
},
"app.modal.install-to-play.install-button": {
"message": "Installer"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# mods}}"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Filen overskrider 1MiB størrelse begrænsning"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.shared-instance": {
"message": "Delt instance"
},
@@ -314,12 +413,33 @@
"app.modal.update-to-play.header": {
"message": "Opdater for at spille"
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Tilføjede"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Fjernede"
},
"app.modal.update-to-play.update-required": {
"message": "Opdatering krævet"
},
"app.project.install-button.already-installed": {
"message": "Dette projekt er allerede installeret"
},
"app.project.install-button.switch-version": {
"message": "Skift version"
},
"app.project.install-context.back-to-browse": {
"message": "Tilbage til udforsk"
},
"app.project.version.all-versions": {
"message": "Alle versioner"
},
"app.project.version.download-in-browser": {
"message": "Download i browser"
},
"app.project.version.installing": {
"message": "Installere"
},
"app.project.versions.already-installed": {
"message": "Allerede installeret"
},
@@ -380,6 +500,9 @@
"app.skins.modal.add-title": {
"message": "Tilføjer et skin"
},
"app.skins.modal.arm-style-section": {
"message": "Arm stil"
},
"app.skins.modal.arm-style-slim": {
"message": "Slank"
},
@@ -482,6 +605,12 @@
"app.skins.title": {
"message": "Skin vælger"
},
"app.skins.toggle-ears-features-off": {
"message": "Slå fra"
},
"app.skins.toggle-ears-features-on": {
"message": "Slå til"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} er færdig med at download. Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
},
@@ -530,6 +659,9 @@
"app.world.world-item.players-online": {
"message": "{count} online"
},
"content.shared-instance.delete-button": {
"message": "Slet alligevel"
},
"friends.action.add-friend": {
"message": "Tilføj en ven"
},
@@ -593,6 +725,27 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Log ind på en Modrinth konto</link> for at tilføje venner og se hvad de spiller!"
},
"instance.action.create-shortcut": {
"message": "Opret genvej"
},
"instance.action.export-modpack": {
"message": "Eksporter modpack"
},
"instance.action.more-actions": {
"message": "Flere handlinger"
},
"instance.action.open-folder": {
"message": "Åbn mappe"
},
"instance.action.settings": {
"message": "Instans indstillinger"
},
"instance.action.starting": {
"message": "Starter..."
},
"instance.action.stopping": {
"message": "Stopper..."
},
"instance.add-server.add-and-play": {
"message": "Tilføj og spil"
},
@@ -635,6 +788,15 @@
"instance.files.save-as": {
"message": "Gem som..."
},
"instance.locked.delete-button": {
"message": "Slet instans"
},
"instance.locked.play-tooltip": {
"message": "Denne instans er blevet låst"
},
"instance.playtime.never-played": {
"message": "Aldrig spillet"
},
"instance.server-modal.address": {
"message": "Adresse"
},
@@ -806,6 +968,33 @@
"instance.settings.tabs.window.width.enter": {
"message": "Indtast bredde..."
},
"instance.shared-instance.error.title": {
"message": "Noget gik galt"
},
"instance.shared-instance.publish-review.added-label": {
"message": "Tilføjede"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Skub opdatering til spillere"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Vælg konfigurations filer"
},
"instance.shared-instance.publish-review.header": {
"message": "Gennemså ændringer"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Skub opdatering"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Fjernede"
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instans låst"
},
"instance.shared-instance.unavailable.title": {
"message": "Delt instans er ikke længere tilgængelig"
},
"instance.worlds.a_minecraft_server": {
"message": "En Minecraft server"
},
@@ -815,6 +1004,9 @@
"instance.worlds.copy_address": {
"message": "Kopier adresse"
},
"instance.worlds.create_shortcut": {
"message": "Opret genvej"
},
"instance.worlds.dont_show_on_home": {
"message": "Vis ikke på forsiden"
},
@@ -866,6 +1058,54 @@
"minecraft-account.sign-in": {
"message": "Log ind på Minecraft"
},
"minecraft-required.description-header": {
"message": "Log ind med en Microsoft konto"
},
"minecraft-required.dont-have-account": {
"message": "Har du ikke en konto?"
},
"minecraft-required.get-minecraft": {
"message": "Få Minecraft"
},
"minecraft-required.get-support": {
"message": "Få hjælp"
},
"minecraft-required.header": {
"message": "Minecraft påkrævet"
},
"minecraft-required.sign-in": {
"message": "Log ind på Microsoft"
},
"modal.modrinth-account-required.cancel-button": {
"message": "Annuller"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Fortsæt i din browser"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Opret en konto"
},
"modal.modrinth-account-required.header": {
"message": "Konto påkrævet"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Åbn browser igen"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Log ind på Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Log ind med en Modrinth konto"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Logger ind"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Har du problemer med at logge ind? <support>Få hjælp</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Venter på browser bekræftigelse..."
},
"search.filter.locked.instance": {
"message": "Givet af instanceen"
},
+6 -21
View File
@@ -15,7 +15,7 @@
"message": "Kopiert"
},
"app.action-bar.install.copy-details": {
"message": "Details kopieren"
"message": "Kopier Details"
},
"app.action-bar.install.dismiss": {
"message": "Ignorieren"
@@ -27,16 +27,16 @@
"message": "Wiederholen"
},
"app.action-bar.install.summary.app-closing": {
"message": "Aufgrund von Schließung der App abgebrochen"
"message": "Abbroche will app gschlosse worde isch"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "Modpack konnte nicht gelesen werden"
"message": "Modpack kud nöd glese werde"
},
"app.action-bar.install.summary.canceled": {
"message": "Abgebrochen"
"message": "Abbroche"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "Bereinigung konnte nicht abgeschlossen werden"
"message": "Ufrume kunnti nöt beendet werde"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Dateien konnten nicht heruntergeladen werden"
@@ -155,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Farbschema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliothek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standard-Startseite"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Deaktiviert das Namensschild über deinem Spieler auf der Skin-Seite."
},
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen"
},
"app.appearance-settings.select-option": {
"message": "Option wählen"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast."
},
@@ -456,7 +441,7 @@
"message": "Falsches Konto"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Diese Person wird für diese geteilte Instanz erhalten keine Updates mehr erhalten"
"message": "Diese Person wird für diese geteilte Instanz keine Updates mehr erhalten"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Jede bereits installierte Kopie bleibt auf deren Gerät erhalten"
+2 -17
View File
@@ -155,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Farbschema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliothek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standard-Startseite"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Deaktiviert das Namensschild über deinem Spieler auf der Skin-Seite."
},
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native Dekorationen"
},
"app.appearance-settings.select-option": {
"message": "Wähle eine Option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen."
},
@@ -396,7 +381,7 @@
"message": "Instanz"
},
"app.instance.mods.bulk-update.downloading-projects": {
"message": "{current, number}/{total, number} Projekte werden heruntergelanden..."
"message": "{current, number}/{total, number} Projekte werden heruntergeladen..."
},
"app.instance.mods.bulk-update.finishing": {
"message": "Update wird fertiggestellt..."
@@ -456,7 +441,7 @@
"message": "Falsches Konto"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Diese Person wird für diese geteilte Instanz erhalten keine Updates mehr erhalten"
"message": "Diese Person wird für diese geteilte Instanz keine Updates mehr erhalten"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Jede bereits installierte Kopie bleibt auf deren Gerät erhalten"
+66 -15
View File
@@ -155,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Color theme"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Change the page to which the launcher opens on."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Library"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Default landing page"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Disables the nametag above your player on the skins page."
},
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native decorations"
},
"app.appearance-settings.select-option": {
"message": "Select an option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Displays how much time you've spent playing an instance."
},
@@ -377,6 +362,15 @@
"app.instance.confirm-delete.admonition-header": {
"message": "This action cannot be undone"
},
"app.instance.confirm-delete.bulk-admonition-body": {
"message": "All data for these {count} instances will be permanently deleted, including their worlds, configs, and all installed content."
},
"app.instance.confirm-delete.bulk-delete-button": {
"message": "Delete {count} instances"
},
"app.instance.confirm-delete.bulk-header": {
"message": "Delete {count} instances"
},
"app.instance.confirm-delete.delete-button": {
"message": "Delete instance"
},
@@ -572,6 +566,45 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Search {count} worlds..."
},
"app.library.group.delete": {
"message": "Delete group"
},
"app.library.group.delete-description": {
"message": "Instances in this group will be ungrouped."
},
"app.library.group.edit-name": {
"message": "Edit group name"
},
"app.library.group.name-empty": {
"message": "Group names cannot be empty."
},
"app.library.group.name-reserved": {
"message": "\"None\" is reserved and cannot be used as a group name."
},
"app.library.group.name-too-long": {
"message": "Group names cannot be longer than 32 characters."
},
"app.library.group.rename-failed": {
"message": "Unable to rename group"
},
"app.library.group.ungrouped": {
"message": "Ungrouped"
},
"app.library.selection.aria-label": {
"message": "Selected instances"
},
"app.library.selection.create-group": {
"message": "Create group"
},
"app.library.selection.deleting": {
"message": "Deleting selected instances"
},
"app.library.selection.remove-from-group": {
"message": "Remove from group"
},
"app.library.selection.selected-count": {
"message": "{count} selected"
},
"app.modal.install-to-play.additional-context": {
"message": "Additional context"
},
@@ -704,6 +737,12 @@
"app.project.versions.already-installed": {
"message": "Already installed"
},
"app.resource-management-settings.always-show-copy-details.description": {
"message": "Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs."
},
"app.resource-management-settings.always-show-copy-details.title": {
"message": "Always show copy details"
},
"app.settings.developer-mode-enabled": {
"message": "Developer mode enabled."
},
@@ -1535,6 +1574,18 @@
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Waiting for browser confirmation..."
},
"onboarding-checklist.create-instance": {
"message": "Create first instance"
},
"onboarding-checklist.login-minecraft": {
"message": "Sign in to Minecraft"
},
"onboarding-checklist.login-modrinth": {
"message": "Sign in to Modrinth"
},
"onboarding-checklist.title": {
"message": "Getting started"
},
"search.filter.locked.instance": {
"message": "Provided by the instance"
},
@@ -137,18 +137,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Color de la interfaz"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que se abre el launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Librería"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página de inicio predeterminada"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desactiva la etiqueta de nombre arriba de tu personaje en la página de skins."
},
@@ -173,9 +161,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
},
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
@@ -71,18 +71,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Tema de color"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que el launcher inicia."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Biblioteca"
},
"app.appearance-settings.default-landing-page.title": {
"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."
},
@@ -107,9 +95,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
},
"app.appearance-settings.select-option": {
"message": "Selecciona una opción"
},
"app.appearance-settings.show-play-time.description": {
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
},
+3 -15
View File
@@ -83,18 +83,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Väriteema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Muuta sivua mille laukaisin avautuu."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Koti"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Kirjasto"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Oletus laskeutumissivu"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Kytkee pois päältä nimikyltin pelaajasi päällä skinit sivulla."
},
@@ -119,9 +107,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Natiivit koristukset"
},
"app.appearance-settings.select-option": {
"message": "Valitse vaihtoehto"
},
"app.appearance-settings.show-play-time.description": {
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
},
@@ -311,6 +296,9 @@
"app.instance.mods.successfully-uploaded": {
"message": "Onnistuneesti ladattu"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Kaikki laitteeseen jo asennetut kopiot säilyvät laitteessa"
},
"app.instance.worlds.add-server": {
"message": "Lisää palvelin"
},
@@ -59,18 +59,6 @@
"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."
},
@@ -95,9 +83,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Native decorations"
},
"app.appearance-settings.select-option": {
"message": "Pumili ng opsiyon"
},
"app.appearance-settings.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
},
+92 -29
View File
@@ -36,7 +36,7 @@
"message": "Annulé"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "Le nettoyage n'a pas terminé"
"message": "Le nettoyage n'a pas pu terminer"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "Impossible de télécharger les fichiers"
@@ -132,7 +132,7 @@
"message": "Tout accepter"
},
"app.ads-consent.body": {
"message": "Les publicités rendent Modrinth possible et financent les rémunérations des créateurs. Nos partenaires peuvent stocker des cookies ou y accéder au sein de l'application afin de personnaliser les publicités et de mesurer les performances."
"message": "Les publicités rendent Modrinth possible et financent la rémunération des créateurs. Nos partenaires peuvent stocker des cookies ou y accéder au sein de l'application afin de personnaliser les publicités et de mesurer les performances."
},
"app.ads-consent.manage": {
"message": "Gérer les préférences"
@@ -155,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Couleur du thème"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Modifier la page sur laquelle le launcher s'ouvre."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Accueil"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliothèque"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Page d'ouverture"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Désactive le nom au-dessus du joueur sur la page skins."
},
@@ -180,7 +168,7 @@
"message": "Mondes sur partie rapide"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiser le lanceur quand Minecraft démarre."
"message": "Minimiser le launcher quand a processus Minecraft démarre."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiser le lanceur"
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Décorations natives"
},
"app.appearance-settings.select-option": {
"message": "Sélectionnez une option"
},
"app.appearance-settings.show-play-time.description": {
"message": "Affiche combien de temps vous avez passé sur une instance."
},
@@ -294,7 +279,7 @@
"message": "Téléchargement de Minecraft"
},
"app.install.phase.downloading_pack_file": {
"message": "Téléchargement le fichier de pack"
"message": "Téléchargement du fichier de pack"
},
"app.install.phase.extracting_overrides": {
"message": "Extraction des fichiers personnalisés"
@@ -522,7 +507,7 @@
"message": "Vous utilisez le mauvais compte Modrinth"
},
"app.instance.worlds.add-server": {
"message": "Ajouter le serveur"
"message": "Ajouter un serveur"
},
"app.instance.worlds.browse-servers": {
"message": "Parcourir des serveurs"
@@ -555,13 +540,13 @@
"message": "Ajouter un serveur ou parcourir pour commencer"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Aucun serveur ou monde ajoutés"
"message": "Aucun serveur ou monde ajouté"
},
"app.instance.worlds.remove-server-modal.remove-button": {
"message": "Enlever le serveur"
"message": "Supprimer le serveur"
},
"app.instance.worlds.remove-server-modal.title": {
"message": "Enlever le serveur"
"message": "Supprimer le serveur"
},
"app.instance.worlds.remove-server-modal.warning-body": {
"message": "Ce serveur sera enlevé de votre liste de serveur et de la liste de serveur en jeu. Vous pourrez le rajouter plus tard si vous connaissez son adresse."
@@ -611,6 +596,9 @@
"app.modal.install-to-play.report-image-too-large": {
"message": "Le fichier dépasse la limite de 1 MiB"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "Pour les notifications DMCA ou autres réclamations juridiques, veuillez consulter notre <copyright-link>Politique relative aux droits d'auteur</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Quelle règle cette instance enfreint-elle ?"
},
@@ -629,6 +617,9 @@
"app.modal.install-to-play.report-submitted": {
"message": "Signalement soumis"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Pour des demandes de support, contactez notre <support-link>équipe de support</support-link>. Pour signaler des bugs, ouvrez <github-link>une issue Github</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Un fichier est seulement examiné s'il est publié sur Modrinth, peu importe son extension (y compris .mrpack)."
},
@@ -638,6 +629,9 @@
"app.modal.install-to-play.shared-instance-content": {
"message": "Contenu de l'instance partagée"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Cette instance partagée contient des fichiers qui ne sont pas publiés sur Modrinth. Nous recommandons fortement de seulement installer des fichiers de sources en qui vous avez confiance."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Ce modpack de serveur contient des fichiers qui ne sont pas publiés sur Modrinth. Nous recommandons fortement de seulement installer des fichiers de sources en qui vous avez confiance."
},
@@ -723,7 +717,7 @@
"message": "Gestion des ressources"
},
"app.skins.add-button": {
"message": "Ajouter une skin"
"message": "Ajouter un skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Glisser et déposer"
@@ -738,7 +732,7 @@
"message": "Cela va supprimer de manière permanente ce skin. Cette action est irréversible."
},
"app.skins.delete-modal.title": {
"message": "Êtes-vous sûr de vouloir supprimer cette skin ?"
"message": "Êtes-vous sûr de vouloir supprimer ce skin ?"
},
"app.skins.dropped-file-error.text": {
"message": "Impossible de lire le fichier déposé."
@@ -753,13 +747,13 @@
"message": "Modifier le skin"
},
"app.skins.modal.add-skin-button": {
"message": "Ajouter une skin"
"message": "Ajouter un skin"
},
"app.skins.modal.add-title": {
"message": "Ajouter une skin"
"message": "Ajouter un skin"
},
"app.skins.modal.arm-style-section": {
"message": "Bras"
"message": "Style du bras"
},
"app.skins.modal.arm-style-slim": {
"message": "Mince"
@@ -929,18 +923,36 @@
"content.shared-instance.change-version-body": {
"message": "Changer la version change seulement votre copie locale. Des futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.delete-bulk-body": {
"message": "Certains projets sélectionnés font partie de l'instance partagée. Les supprimer changerait seulement votre copie locale, et de futures mises à jour d'instance partagées peuvent restaurer ou les changer de nouveau."
},
"content.shared-instance.delete-button": {
"message": "Supprimer quand même"
},
"content.shared-instance.delete-many-button": {
"message": "Supprimer {count, number} projets quand même"
},
"content.shared-instance.delete-single-body": {
"message": "Le supprimer change seulement votre copie locale. De futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.disable-bulk-body": {
"message": "Certains projets sélectionnés font partie de l'instance partagée. Les désactiver changerait seulement votre copie locale, et de futures mises à jour d'instance partagées peuvent restaurer ou les changer de nouveau."
},
"content.shared-instance.disable-button": {
"message": "Désactiver quand même"
},
"content.shared-instance.disable-many-button": {
"message": "Désactiver {count, number} projets quand même"
},
"content.shared-instance.disable-single-body": {
"message": "Le désactiver change seulement votre copie locale. De futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.unlink-body": {
"message": "Le délier change seulement votre copie locale. De futures mises à jour d'instance partagées peuvent restaurer ou changer la version."
},
"content.shared-instance.warning-header": {
"message": "Ceci fait partie de l'instance partagée"
},
"friends.action.add-friend": {
"message": "Ajouter un ami"
},
@@ -975,7 +987,7 @@
"message": "La demande d'ami a été envoyé"
},
"friends.friend.view-profile": {
"message": "Voir le profile"
"message": "Voir le profil"
},
"friends.heading": {
"message": "Amis"
@@ -1004,6 +1016,48 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Créer un compte Modrinth</link> pour ajouter des amis et voir à quoi ils jouent !"
},
"installation-settings.shared-instance.linked-title": {
"message": "Instance partagée liée"
},
"installation-settings.shared-instance.title": {
"message": "Dépublier l'instance"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Dissocier l'instance partagée"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Déconnecter cette instance locale de futures mises à jour partagées."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Dissociation..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Dépublier l'instance partagée"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Supprime cette instance partagée de Modrinth et arrête d'envoyer des mises à jour à toute personne l'utilisant. Votre copie locale ne sera pas affectée."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Dépublication..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Cela affecte seulement votre instance locale. Votre contenu installé restera sur cet appareil, et l'instance partagée et les autres personnes l'utilisant ne seront pas affectés."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Dissociation de l'instance partagée"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Dissocier l'instance partagée"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Cela supprime l'instance partagée des serveurs de Modrinth. Les personnes l'utilisant dans l'application Modrinth vont cesser de recevoir des mises à jour, mais votre instance locale et son contenu resteront sur cet appareil."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Dépublication de l'instance partagée"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Dépublier l'instance partagée"
},
"instance.action.create-shortcut": {
"message": "Créer un raccourci"
},
@@ -1217,6 +1271,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "Version de {loader}"
},
"instance.settings.tabs.installation.locked": {
"message": "Les paramètres d'installation ne sont pas disponible pendant que cette instance est verrouillée."
},
"instance.settings.tabs.java": {
"message": "Java et mémoire"
},
@@ -1319,9 +1376,15 @@
"instance.shared-instance.tooltip": {
"message": "Le contenu de cette instance est gérée par quelqu'un d'autre."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Votre accès à l'instance partagée a été révoqué. Cette instance reste disponible, mais n'est plus liée et ne recevra plus de mises à jour."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "L'instance principale a été supprimée. Cette instance est encore disponible, mais n'est plus liée et va cesser de recevoir des mises à jour."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Cette instance partagée a été verrouillée par la Team de Modération de Contenu. Elle ne recevra plus de mises à jour de l'instance primaire et ne peut plus être jouée."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instance verrouillée"
},
+126 -30
View File
@@ -108,7 +108,7 @@
"message": "Elsődleges játékpéldány"
},
"app.action-bar.reload-to-update": {
"message": "Töltsd újra a frissítéshez"
"message": "Frissítés elérhető"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó pédányok megjelenítése"
@@ -155,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Téma"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Állítsd be, hogy az indító melyik oldalon nyíljon meg."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Kezdőlap"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Könyvtár"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Alapértelmezett nyitóoldal"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Letiltja a játékosod felett megjelenő névcímkét a Kinézetek oldalon."
},
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Rendszer ablakkeret használata"
},
"app.appearance-settings.select-option": {
"message": "Válassz egy lehetőséget"
},
"app.appearance-settings.show-play-time.description": {
"message": "Megmutatja, mennyi időt töltöttél egy játékpéldányban."
},
@@ -315,7 +300,7 @@
"message": "A Java {version} kibontása..."
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "A Java {version} előkészítése..."
"message": "A Java {version} beszerzése..."
},
"app.install.phase.preparing_java.resolving": {
"message": "A Java {version} előkészítése..."
@@ -324,25 +309,28 @@
"message": "A Java {version} ellenőrzése..."
},
"app.install.phase.reading_pack_manifest": {
"message": "Csomag adati kiolvasása"
"message": "A modcsomag leírófájljának beolvasása..."
},
"app.install.phase.resolving_loader": {
"message": "A betöltő meghatározása..."
},
"app.install.phase.resolving_minecraft": {
"message": "Minecraft Betöltése"
"message": "A Minecraft-verzió meghatározása..."
},
"app.install.phase.resolving_pack": {
"message": "Tartalom betöltése"
"message": "A tartalom meghatározása..."
},
"app.install.phase.rolling_back": {
"message": "Visszavonás"
"message": "Visszavonás..."
},
"app.install.phase.running_loader_processors": {
"message": "Betöltő folyamatok futtatása"
"message": "Betöltőfolyamatok futtatása..."
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Hozzáadva"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "A helyi példányod előrébb tart, mint azoké a felhasználóké, akikkel megosztottad."
"message": "A te gépeden lévő játékpéldány újabb, mint azoké a felhasználóké, akikkel megosztottad."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "A változtatásaid még nem lettek megosztva"
@@ -360,7 +348,7 @@
"message": "Frissítés közzététele"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "A példányt használó összes résztvevővel megosztásra kerülő tartalomváltoztatások áttekintése."
"message": "A játékpéldányt használó összes résztvevővel megosztásra kerülő tartalomváltoztatások áttekintése."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Változtatások áttekintése"
@@ -405,7 +393,7 @@
"message": "projekt"
},
"app.instance.mods.locked-content": {
"message": "Zárolt példányokban a tartalom nem változtatható meg."
"message": "Zárolt játékpéldányokban a tartalom nem változtatható meg."
},
"app.instance.mods.project-was-added": {
"message": "„{name}” hozzá lett adva"
@@ -423,7 +411,7 @@
"message": "Sikeresen feltöltve"
},
"app.instance.share.empty.description": {
"message": "Ezt a példányt megoszthatod a barátaiddal!"
"message": "Ezt a játékpéldányt megoszthatod a barátaiddal!"
},
"app.instance.share.empty.heading": {
"message": "Nincsenek barátok meghívva"
@@ -452,20 +440,65 @@
"app.instance.share.locked.wrong-account-heading": {
"message": "Helytelen fiók"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Többé nem fog frissítéseket kapni ehhez a megosztott játékpéldányhoz"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "A már telepített játékpéldányok megmaradnak az eszközén"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Később újra meghívhatod"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Ő az utolsó felhasználó, a megosztás ezen az játékpéldányon le lesz tiltva"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Mi fog történni?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Hozzáférés visszavonása"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Hozzáférés visszavonása"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username} avatárja"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Ha visszavonod {username} hozzáférését ehhez a megosztott játékpéldányhoz, újra meg kell hívnod, mielőtt frissítéseket kaphatna."
},
"app.instance.share.sign-in.button": {
"message": "Bejelentkezés"
},
"app.instance.share.unlink.body": {
"message": "A játékpéldány megosztásához le kell választanod ezt a modcsomagot"
},
"app.instance.share.unlink.header": {
"message": "A megosztáshoz le kell választani"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Telepítés mindenképpen"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Példány"
"message": "Játékpéldány"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "az összekapcsolt fiók"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": ", hogy kezelhesd ezt a megosztott játékpéldányt. Nem fogod tudni a frissítéseket eljuttatni a felhasználókhoz."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Bejelentkezés mint"
"message": "Jelentkezz be mint"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Be kell jelentkezned a Modrinth-ba"
"message": "Be kell jelentkezned a Modrinthba"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": ", hogy értesítéseket kapj erről a megosztott játékpéldányról."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Nem a megfelelő Modrinth-fiókot használod"
},
"app.instance.worlds.add-server": {
"message": "Szerver hozzáadása"
@@ -518,6 +551,18 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Keresés {count} világ között..."
},
"app.modal.install-to-play.additional-context": {
"message": "További kontextus"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "A játékpéldány, amelyet bejelenteni kívánsz"
},
"app.modal.install-to-play.delete-instance": {
"message": "Játékpéldány törlése"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Ne telepítse"
},
"app.modal.install-to-play.header": {
"message": "Telepítés a játékhoz"
},
@@ -533,6 +578,9 @@
"app.modal.install-to-play.mod-count": {
"message": "{count} mod"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.reviewed-files": {
"message": "Egy fájlt csak akkor vizsgálnak meg, ha azt a Modrinth-en közzétették, függetlenül a fájlformátumtól (beleértve a .mrpack formátumot is)."
},
@@ -783,7 +831,7 @@
"message": "Sikeres letöltés"
},
"app.update-popup.reload": {
"message": "Töltsd újra a frissítéshez"
"message": "Frissítés"
},
"app.update-popup.title": {
"message": "Frissítés elérhető"
@@ -875,6 +923,15 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Lépj be Modrinth fiókodba</link>, hogy felvehess barátokat és lásd mivel játszanak!"
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Közzététel visszavonása..."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Megosztott játékpéldány leválasztása"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Megosztott játékpéldány leválasztása"
},
"instance.action.create-shortcut": {
"message": "Parancsikon létrehozása"
},
@@ -944,6 +1001,9 @@
"instance.files.save-as": {
"message": "Mentés másként..."
},
"instance.locked.delete-button": {
"message": "Játékpéldány törlése"
},
"instance.playtime.never-played": {
"message": "Még nem játszott"
},
@@ -1121,6 +1181,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/eleresi/ut/a/javahoz"
},
"instance.settings.tabs.sharing": {
"message": "Megosztás"
},
"instance.settings.tabs.window": {
"message": "Ablak"
},
@@ -1151,6 +1214,18 @@
"instance.settings.tabs.window.width.enter": {
"message": "Szélesség megadása..."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Hozzáadva"
},
"instance.shared-instance.publish-review.header": {
"message": "Változtatások áttekintése"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Eltávolítva"
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Pédány lezárva"
},
"instance.worlds.a_minecraft_server": {
"message": "Egy Minecraft szerver"
},
@@ -1235,6 +1310,27 @@
"minecraft-required.sign-in": {
"message": "Bejelentkezés a Microsft-ba"
},
"modal.modrinth-account-required.cancel-button": {
"message": "Mégse"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Fiók létrehozása"
},
"modal.modrinth-account-required.header": {
"message": "Fiók szükséges"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Jelentkezz be a Modrinth-be"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Jelentkezz be egy Modrinth-fiókba"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Bejelentkezés"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Várakozás a böngésző megerősítésére..."
},
"search.filter.locked.instance": {
"message": "A játékpéldány által van megadva"
},
@@ -56,18 +56,6 @@
"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.default-landing-page.title": {
"message": "Halaman arahan bawaan"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Mematikan label nama di atas wujud pemain Anda pada halaman rupa."
},
@@ -92,9 +80,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
},
+3 -18
View File
@@ -27,7 +27,7 @@
"message": "Riprova"
},
"app.action-bar.install.summary.app-closing": {
"message": "Annullato per app chiusa"
"message": "Annullato per chiusura app"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "Impossibile leggere il pacchetto"
@@ -155,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Tema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Imposta la pagina che si aprirà all'avvio del launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Libreria"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Pagina di avvio"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Rimuove il nametag sopra la testa del giocatore nella pagina delle skin."
},
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Seleziona"
},
"app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai speso giocando a un'istanza."
},
@@ -489,7 +474,7 @@
"message": "Scollega prima di condividere"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Questa istanza condivisa è già installata come <bold>{instanceName}</bold>. Vuoi installarne un'altra copia?"
"message": "Questa istanza condivisa è già installata come <bold>{instanceName}</bold>. Vuoi davvero installarne un'altra copia?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Istanza condivisa già installata"
@@ -972,7 +957,7 @@
"message": "Stringi un'amicizia"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {# richiesta} other {# richieste}} d''amicizia"
"message": "{count, plural, one {# richiesta} other {# richieste}} d'amicizia"
},
"friends.add-friend.submit": {
"message": "Invia richiesta d'amicizia"
+259 -16
View File
@@ -152,18 +152,6 @@
"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": "スキンページでプレイヤー上部のネームタグを非表示にします。"
},
@@ -188,9 +176,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "OSのウィンドウUIを使用"
},
"app.appearance-settings.select-option": {
"message": "オプションを選択してください"
},
"app.appearance-settings.show-play-time.description": {
"message": "インスタンスのプレイ時間を表示します。"
},
@@ -338,6 +323,24 @@
"app.install.phase.running_loader_processors": {
"message": "ローダーのプロセッサーを実行中"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "追加済"
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "変更はまだ共有されていません"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "アップデートを公開する"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "公開しています…"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "アップデートを公開する"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "審査中…"
},
"app.instance.confirm-delete.admonition-body": {
"message": "インスタンス内のすべてのデータは、ワールド、設定、インストール済みのコンテンツを含め、完全に削除されます。"
},
@@ -374,6 +377,9 @@
"app.instance.mods.content-type-project": {
"message": "プロジェクト"
},
"app.instance.mods.locked-content": {
"message": "ロックされた起動構成の内にあるコンテンツは変更できません。"
},
"app.instance.mods.project-was-added": {
"message": "「{name}」が追加されました"
},
@@ -389,6 +395,90 @@
"app.instance.mods.successfully-uploaded": {
"message": "アップロードに成功しました"
},
"app.instance.share.empty.description": {
"message": "フレンドとこのインスタンスをシェアしましょう!"
},
"app.instance.share.empty.heading": {
"message": "まだ誰も招待していないようです"
},
"app.instance.share.empty.invite-friends-button": {
"message": "フレンドを招待"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} をシェアする"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "このページにアクセスするには"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "としてサインインする必要があります。"
},
"app.instance.share.locked.linked-account-fallback": {
"message": "リンクされたアカウント"
},
"app.instance.share.locked.signed-out-heading": {
"message": "サインインしていません"
},
"app.instance.share.locked.switch-account-button": {
"message": "アカウントを切り替える"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "アカウントが違います"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "このインスタンスのアップデートは受信できなくなります"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "後でもう一度招待することもできます"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "彼らがすでにインストールしたコピーは彼らのデバイスに残ります"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "この変更で何が起きるか?"
},
"app.instance.share.remove-user-modal.header": {
"message": "アクセスを取り消す"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "アクセスを取り消す"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}のアバター"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "もし{username}のアクセス権を取り消した場合、彼らが再びアップデートを受け取れるようにするには、もう一度招待し直す必要があります。"
},
"app.instance.share.sign-in.button": {
"message": "サインイン"
},
"app.instance.share.unlink.body": {
"message": "この起動構成を共有するには、このmodpackのリンクを解除する必要があります"
},
"app.instance.share.unlink.header": {
"message": "共有にはリンクを解除する必要があります"
},
"app.instance.shared-instance-already-installed.body": {
"message": "この共有された起動構成はすでに<bold> {instanceName} </bold>としてインストールされています。別のコピーをインストールしてもよろしいですか?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "共有された起動構成はすでにインストールされています"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "インストールを続行"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "起動構成"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "リンクされたアカウント"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Modrinthへのサインインが必要です"
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Modrinthのアカウントが間違っています"
},
"app.instance.worlds.add-server": {
"message": "サーバーを追加"
},
@@ -440,11 +530,26 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "{count} 個のワールドを検索…"
},
"app.modal.install-to-play.additional-context": {
"message": "追加のコンテンツ"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "可能であれば、リンクや画像も含めてください"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "起動構成を報告する"
},
"app.modal.install-to-play.delete-instance": {
"message": "インスタンスを削除する"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "インストールしない"
},
"app.modal.install-to-play.header": {
"message": "インストールしてプレイ"
},
"app.modal.install-to-play.install-anyway": {
"message": "このままインストール"
"message": "インストールを続行"
},
"app.modal.install-to-play.install-button": {
"message": "インストール"
@@ -455,12 +560,48 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {#個のMod}}"
},
"app.modal.install-to-play.report-description": {
"message": "このフォームを使用して、<rules-link>ルール</rules-link>または<terms-link>利用規約</terms-link>に違反する可能性のある事例を報告してください。"
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "対応していない画像形式です"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "ファイルのサイズが 1 MiBの制限を超えています"
},
"app.modal.install-to-play.report-reason": {
"message": "この起動構成はどの規則に違反していますか?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "不適切なコンテンツ"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "悪意がある"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "スパム"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "共有された起動構成を通報する"
},
"app.modal.install-to-play.report-submitted": {
"message": "通報は送信されました"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "サポートに関するお問い合わせは、<support-link>サポートチーム</support-link>までご連絡ください。バグの報告は、<github-link>GitHubのIssue</github-link>を作成してください。"
},
"app.modal.install-to-play.reviewed-files": {
"message": "ファイル形式(.mrpackを含む)に関わらず、Modrinthに公開されているファイルのみが審査の対象となります。"
},
"app.modal.install-to-play.shared-instance": {
"message": "共有インスタンス"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "共有された起動構成の内容"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "この共有された起動構成には、Modrinthで公開されていないファイルが含まれています。信頼できる提供元からのファイルのみをインストールすることを強くお勧めします。"
},
"app.modal.install-to-play.unknown-files-description": {
"message": "このサーバーModパックには、Modrinthに公開されていないファイルが含まれています。信頼できるソースからのファイルのみをインストールすることを強く推奨します。"
},
@@ -479,6 +620,24 @@
"app.modal.update-to-play.removed": {
"message": "削除されました"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "このサーバーModパックのアップデートには、Modrinthに公開されていないファイルが含まれています。信頼できるソースからのファイルのみをインストールすることを強く推奨します。"
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "追加済み"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "削除済み"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "この共有された起動構成のアップデートには、Modrinthで公開されていないファイルが含まれています。信頼できる提供元からのファイルのみをインストールすることを強くお勧めします。"
},
"app.modal.update-to-play.update-required": {
"message": "更新が必要です"
},
"app.modal.update-to-play.update-required-description": {
"message": "{name}をプレイするにはアップデートが必要です。ゲームを起動するには最新バージョンにアップデートしてください。"
},
"app.project.install-button.already-installed": {
"message": "このプロジェクトは既にインストールされています"
},
@@ -734,6 +893,36 @@
"app.world.world-item.players-online": {
"message": "{count} 人がオンライン"
},
"content.shared-instance.change-version-body": {
"message": "バージョンの変更はローカルコピーにのみ影響します。今後共有された起動構成がアップデートされた際に、元の状態に戻ったり、別のバージョンに変更されたりする場合があります。"
},
"content.shared-instance.delete-bulk-body": {
"message": "選択された一部のプロジェクトは共有された起動構成の一部です。これらを削除してもローカルコピーのみが変更され、今後共有された起動構成がアップデートされた際に、再び復元されたり変更されたりする場合があります。"
},
"content.shared-instance.delete-button": {
"message": "とにかく削除する"
},
"content.shared-instance.delete-many-button": {
"message": "それでも {count, number} のプロジェクトを削除する"
},
"content.shared-instance.delete-single-body": {
"message": "削除はローカルコピーにのみ影響します。今後共有された起動構成がアップデートされた際に、元の状態に戻ったり、別のバージョンに変更されたりする場合があります。"
},
"content.shared-instance.disable-bulk-body": {
"message": "選択された一部のプロジェクトは共有された起動構成の一部です。これらを無効化してもローカルコピーのみが変更され、今後共有された起動構成がアップデートされた際に、再び有効化されたり、復元されたり、変更されたりする場合があります。"
},
"content.shared-instance.disable-button": {
"message": "とにかく無効化する"
},
"content.shared-instance.disable-many-button": {
"message": "それでも {count, number} のプロジェクトを無効化する"
},
"content.shared-instance.disable-single-body": {
"message": "無効化はローカルコピーにのみ影響します。今後共有された起動構成がアップデートされた際に、再び有効化されたり、元の状態に戻ったり、変更されたりする場合があります。"
},
"content.shared-instance.warning-header": {
"message": "これは共有された起動構成の一部です"
},
"friends.action.add-friend": {
"message": "フレンドを追加"
},
@@ -797,6 +986,21 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Modrinthアカウントにサインイン</link>して友達を追加し、彼らがプレイしているゲームをチェックしよう!"
},
"installation-settings.shared-instance.linked-title": {
"message": "リンク済の共有された起動構成"
},
"installation-settings.shared-instance.title": {
"message": "起動構成を非公開化"
},
"installation-settings.shared-instance.unlink-button": {
"message": "起動構成の共有を解除"
},
"installation-settings.shared-instance.unlinking-button": {
"message": "共有を解除中…"
},
"installation-settings.shared-instance.unpublish-button": {
"message": "共有された軌道構成を非公開にする"
},
"instance.action.create-shortcut": {
"message": "ショートカット作成"
},
@@ -1073,6 +1277,9 @@
"instance.settings.tabs.window.width.enter": {
"message": "幅を入力…"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "アップデートを配信する"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraftサーバー"
},
@@ -1157,6 +1364,42 @@
"minecraft-required.sign-in": {
"message": "Microsoft にサインイン"
},
"modal.modrinth-account-required.browser-description": {
"message": "サインイン用のタブが開きました。そちらでサインインを完了してから、アプリに戻ってください。"
},
"modal.modrinth-account-required.cancel-button": {
"message": "キャンセル"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "ブラウザで続行"
},
"modal.modrinth-account-required.create-account-button": {
"message": "アカウントを作成"
},
"modal.modrinth-account-required.description": {
"message": "この機能を利用するには、Modrinthアカウントにログインする必要があります。"
},
"modal.modrinth-account-required.header": {
"message": "アカウントが必要です"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "もう一度ブラウザを開く"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Modrinthへサインイン"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Modrinth アカウントにサインインしてください"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "サインイン"
},
"modal.modrinth-account-required.support-prompt": {
"message": "サインインでお困りですか?<support>サポート</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "ブラウザからの確認を待っています…"
},
"search.filter.locked.instance": {
"message": "インスタンスによる条件"
},
+180 -15
View File
@@ -89,6 +89,9 @@
"app.action-bar.install.unknown-instance": {
"message": "알 수 없는 인스턴스"
},
"app.action-bar.install.updating-shared-content": {
"message": "공유 콘텐츠 업데이트 중"
},
"app.action-bar.installs": {
"message": "설치"
},
@@ -152,18 +155,6 @@
"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": "스킨 화면에서 플레이어 위에 있는 이름표를 비활성화합니다."
},
@@ -188,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "네이티브 제목 표시줄"
},
"app.appearance-settings.select-option": {
"message": "옵션 선택"
},
"app.appearance-settings.show-play-time.description": {
"message": "인스턴스의 플레이 시간을 표시합니다."
},
@@ -338,6 +326,36 @@
"app.install.phase.running_loader_processors": {
"message": "로더 진행기 실행 중"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "추가됨"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "현재 로컬 인스턴스가 콘텐츠를 공유한 다른 사용자들의 인스턴스보다 최신 상태입니다."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "변경 사항이 아직 공유되지 않았습니다"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "업데이트 게시"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "게시중..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "삭제중"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Push 업데이트"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "이 인스턴스를 사용하는 모든 사람과 공유될 콘텐츠 변경 사항을 검토하세요."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Review changes"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "검토중..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "인스턴스의 모든 데이터가 삭제됩니다. 세계 폴더, 설정 폴더 등 설치된 모든 컨텐츠가 포함됩니다."
},
@@ -374,6 +392,9 @@
"app.instance.mods.content-type-project": {
"message": "프로젝트"
},
"app.instance.mods.locked-content": {
"message": "잠긴 인스턴스의 콘텐츠는 변경할 수 없습니다."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" 추가됨"
},
@@ -389,6 +410,102 @@
"app.instance.mods.successfully-uploaded": {
"message": "업로드 성공"
},
"app.instance.share.empty.description": {
"message": "친구들과 이 인스턴스를 공유할 수 있습니다!"
},
"app.instance.share.empty.heading": {
"message": "아직 초대된 친구가 없습니다"
},
"app.instance.share.empty.invite-friends-button": {
"message": "친구 초대"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} 공유"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "이 페이지에 접근하려면"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "계정으로 로그인해야 합니다."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "연동된"
},
"app.instance.share.locked.signed-out-heading": {
"message": "로그인되지 않음"
},
"app.instance.share.locked.switch-account-button": {
"message": "계정 전환"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "잘못된 계정"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "이들은 더 이상 해당 공유 인스턴스의 업데이트를 받지 않습니다"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "이미 설치한 사본은 해당 기기에 그대로 남아 있습니다"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "나중에 다시 초대할 수 있습니다"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "마지막 사용자이며, 이 인스턴스의 공유가 비활성화됩니다"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "어떻게 되나요?"
},
"app.instance.share.remove-user-modal.header": {
"message": "액세스 권한 취소"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "액세스 권한 취소"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}의 계정"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "{username} 님의 이 공유 인스턴스 액세스 권한을 취소하면, 해당 사용자가 업데이트를 받기 전에 다시 초대해야 합니다."
},
"app.instance.share.sign-in.button": {
"message": "로그인"
},
"app.instance.share.unlink.body": {
"message": "인스턴스를 공유하려면 이 모드팩 연동을 해제해야 합니다"
},
"app.instance.share.unlink.header": {
"message": "공유를 진행하려면 연동 해제가 필요합니다"
},
"app.instance.shared-instance-already-installed.body": {
"message": "이 공유 인스턴스는 이미 <bold>{instanceName}</bold> 이름으로 설치되어 있습니다. 사본을 하나 더 설치하시겠습니까?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "공유 인스턴스가 이미 설치됨"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "그래도 설치"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "인스턴스"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "연동된 계정"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "이 공유 인스턴스를 관리해야 합니다. 그렇지 않으면 사용자에게 업데이트를 푸시할 수 없습니다."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "계정 로그인:"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Modrinth 로그인 필요"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "이 공유 인스턴스의 업데이트를 받으려면 로그인하세요."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "잘못된 Modrinth 계정을 사용하고 있습니다"
},
"app.instance.worlds.add-server": {
"message": "서버 추가"
},
@@ -440,6 +557,21 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "{count}개의 세계 검색..."
},
"app.modal.install-to-play.additional-context": {
"message": "추가 정보"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "관련이 있는 경우 가능하면 링크 및 이미지를 포함하세요"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "신고 대상 인스턴스"
},
"app.modal.install-to-play.delete-instance": {
"message": "인스턴스 삭제"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "설치하지 않음"
},
"app.modal.install-to-play.header": {
"message": "설치하고 플레이"
},
@@ -455,6 +587,9 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {모드 #개} other {모드 #개}}"
},
"app.modal.install-to-play.report-description": {
"message": "본 서식을 사용하여 당사의 <rules-link>규칙</rules-link> 또는 <terms-link>이용약관</terms-link> 위반 가능성이 있는 인스턴스를 신고하세요."
},
"app.modal.install-to-play.reviewed-files": {
"message": "모든 파일은 형식(.mrpack 포함)에 무관하게 Modrinth에 게시되어야만 검수를 거칩니다."
},
@@ -1073,6 +1208,36 @@
"instance.settings.tabs.window.width.enter": {
"message": "너비 입력..."
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "플레이어에게 업데이트 게시"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "설정 파일 선택"
},
"instance.shared-instance.publish-review.description": {
"message": "콘텐츠 변경 사항을 검토하고 이번 업데이트에 포함할 설정 파일을 선택하세요."
},
"instance.shared-instance.publish-review.header": {
"message": "변경 사항 검토"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "업데이트 푸시"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "제거됨"
},
"instance.shared-instance.tooltip": {
"message": "이 인스턴스의 콘텐츠는 다른 사용자가 관리하고 있습니다."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "공유 인스턴스 접근 권한이 취소되었습니다. 이 인스턴스는 계속 사용 가능하지만, 더 이상 연동되지 않으며 업데이트를 받지 않습니다."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "기본 인스턴스가 삭제되었습니다. 이 인스턴스는 계속 사용 가능하지만, 더 이상 연동되지 않으며 업데이트를 받지 않습니다."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "이 공유 인스턴스는 콘텐츠 조정 팀에 의해 잠겼습니다. 기본 인스턴스 업데이트를 받지 않으며 플레이할 수 없습니다."
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft 서버"
},
@@ -56,18 +56,6 @@
"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."
},
@@ -92,9 +80,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih satu pilihan"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Melangkau pengesahan berisiko rendah secara automatik seperti pemasangan pek mod pendua, pemadaman kandungan biasa, kemas kini pukal, menyahpautkan pek mod dan gesaan pembaikan. Amaran berbahaya masih akan ditunjukkan."
},
+389 -26
View File
@@ -89,11 +89,14 @@
"app.action-bar.install.unknown-instance": {
"message": "Onbekende instantie"
},
"app.action-bar.install.updating-shared-content": {
"message": "Gedeelde inhoud bijwerken"
},
"app.action-bar.installs": {
"message": "Installaties"
},
"app.action-bar.make-primary-instance": {
"message": "Maak primaire instantie"
"message": "Tot hoofdinstantie maken"
},
"app.action-bar.no-instances-running": {
"message": "Geen actieve instanties"
@@ -102,7 +105,7 @@
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primaire instantie"
"message": "Hoofdinstantie"
},
"app.action-bar.reload-to-update": {
"message": "Herlaadt om te updaten"
@@ -152,18 +155,6 @@
"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": "Start"
},
"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."
},
@@ -188,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Oorspronkelijke versieringen"
},
"app.appearance-settings.select-option": {
"message": "Selecteer een optie"
},
"app.appearance-settings.show-play-time.description": {
"message": "Toont hoeveel tijd je aan een instantie hebt besteed."
},
@@ -338,6 +326,36 @@
"app.install.phase.running_loader_processors": {
"message": "Loader-processors aan het uitvoeren"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Toegevoegd"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Je lokale instantie loopt voor op de gebruikers met wie je deze hebt gedeeld."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Je wijzigingen zijn nog niet gedeeld"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Update verzenden"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Verzenden..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Verwijderd"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Update verzenden"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Bekijk de inhoudelijke wijzigingen die met alle gebruikers van deze instantie zullen worden gedeeld."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Wijzigingen bekijken"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Controleren..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle gegevens van je instantie worden definitief verwijderd, inclusief je werelden, configuraties en alle geïnstalleerde inhoud."
},
@@ -374,6 +392,9 @@
"app.instance.mods.content-type-project": {
"message": "project"
},
"app.instance.mods.locked-content": {
"message": "De inhoud van vergrendelde instanties kan niet worden gewijzigd."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" was toegevoegd"
},
@@ -389,6 +410,102 @@
"app.instance.mods.successfully-uploaded": {
"message": "Succesvol geüpload"
},
"app.instance.share.empty.description": {
"message": "Je kunt deze instantie met je vrienden delen!"
},
"app.instance.share.empty.heading": {
"message": "Geen vrienden uitgenodigd"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Vrienden uitnodigen"
},
"app.instance.share.invite-modal.heading": {
"message": "{name} delen"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Je moet je aanmelden als"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "om deze pagina te openen."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "het gekoppelde account"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Niet aangemeld"
},
"app.instance.share.locked.switch-account-button": {
"message": "Van account wisselen"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Verkeerd account"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Ze zullen geen updates meer ontvangen voor deze gedeelde instantie"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Elke versie die ze al hebben geïnstalleerd, blijft op hun apparaat staan"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Je kunt ze later opnieuw uitnodigen"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Dit is de laatste gebruiker; delen wordt voor deze instantie uitgeschakeld"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Wat gebeurt er dan?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Toegang intrekken"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Toegang intrekken"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "Avatar van {username}"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Als je de toegang van {username} tot deze gedeelde instantie intrekt, moet je die persoon opnieuw uitnodigen voordat die updates kunnen ontvangen."
},
"app.instance.share.sign-in.button": {
"message": "Aanmelden"
},
"app.instance.share.unlink.body": {
"message": "Je moet dit modpack ontkoppelen om je instantie te kunnen delen"
},
"app.instance.share.unlink.header": {
"message": "Om te delen moet je ontkoppelen"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Deze gedeelde instantie is al geïnstalleerd als <bold>{instanceName}</bold>. Weet je zeker dat je nog een exemplaar wilt installeren?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Gedeelde instantie al geïnstalleerd"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Toch installeren"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instantie"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "het gekoppelde account"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "om deze gedeelde instantie te beheren. Je kunt geen updates naar gebruikers verzenden."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Log in als"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Je moet je aanmelden bij Modrinth"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "om updates voor deze gedeelde instantie te ontvangen."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Je gebruikt het verkeerde Modrinth-account"
},
"app.instance.worlds.add-server": {
"message": "Voeg server toe"
},
@@ -440,6 +557,21 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Zoek werelden"
},
"app.modal.install-to-play.additional-context": {
"message": "Aanvullende context"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Voeg links en afbeeldingen toe als dat mogelijk en relevant is"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "Instantie die je meldt"
},
"app.modal.install-to-play.delete-instance": {
"message": "Instantie verwijderen"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Niet installeren"
},
"app.modal.install-to-play.header": {
"message": "Installeer om te spelen"
},
@@ -455,12 +587,51 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural,one {# mod}other {# mods}}"
},
"app.modal.install-to-play.report-description": {
"message": "Gebruik dit formulier om instanties te melden die mogelijk in strijd zijn met onze <rules-link>Regels</rules-link> of <terms-link>Gebruiksvoorwaarden</terms-link>."
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "Bestand is geen ondersteund afbeeldingstype"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Bestand overschrijdt maximale grootte van 1 MiB"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "Voor DMCA-meldingen of andere juridische vorderingen, zie ons <copyright-link>auteursrechtbeleid</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Welke regel wordt in deze instantie overtreden?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Ongepast"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Kwaadaardig"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Gedeelde instantie melden"
},
"app.modal.install-to-play.report-submitted": {
"message": "Melding ingediend"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Neem voor ondersteuningsverzoeken contact op met ons <support-link>ondersteuningsteam</support-link>. Voor bugmeldingen kun je een <github-link>GitHub-issue</github-link> aanmaken."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Een bestand wordt alleen beoordeeld als het op Modrinth wordt gepubliceerd, ongeacht het bestandsformaat (inclusief .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Gedeelde instantie"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Inhoud van de gedeelde instantie"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Deze gedeelde instantie bevat bestanden die niet op Modrinth zijn gepubliceerd. We raden je ten zeerste aan om alleen bestanden te installeren uit bronnen die je vertrouwt."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Dit server-modpack bevat bestanden die niet op Modrinth zijn gepubliceerd. We raden je ten zeerste aan om alleen bestanden te installeren uit bronnen die je vertrouwt."
},
@@ -479,6 +650,24 @@
"app.modal.update-to-play.removed": {
"message": "Verwijderde"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Deze update voor de server-modpack bevat bestanden die niet op Modrinth zijn gepubliceerd. We raden je ten zeerste aan om alleen bestanden te installeren uit bronnen die je vertrouwt."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Toegevoegd"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Verwijderd"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Deze update voor de gedeelde instantie bevat bestanden die niet op Modrinth zijn gepubliceerd. We raden je ten zeerste aan om alleen bestanden te installeren uit bronnen die je vertrouwt."
},
"app.modal.update-to-play.update-required": {
"message": "Update vereist"
},
"app.modal.update-to-play.update-required-description": {
"message": "Er is een update nodig om {name} te spelen. Werk het spel bij naar de nieuwste versie om het te kunnen starten."
},
"app.project.install-button.already-installed": {
"message": "Dit project is al geïnstalleerd"
},
@@ -543,7 +732,7 @@
"message": "De geselecteerde skin wordt permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt."
},
"app.skins.delete-modal.title": {
"message": "Weet u zeker dat u deze skin wilt verwijderen?"
"message": "Weet je zeker dat je deze skin wilt verwijderen?"
},
"app.skins.dropped-file-error.text": {
"message": "Kan het neergezette bestand niet lezen."
@@ -612,7 +801,7 @@
"message": "Voorvertonen"
},
"app.skins.rate-limit.text": {
"message": "U verandert te snel van skin. Mojang's servers hebben nieuwe aanvragen tijdelijk geblokkeerd. Wacht een moment voordat u het opnieuw probeert."
"message": "Je verandert je skin te vaak. De servers van Mojang hebben verdere verzoeken tijdelijk geblokkeerd. Wacht even voordat je het opnieuw probeert."
},
"app.skins.rate-limit.title": {
"message": "Rustig aan!"
@@ -645,7 +834,7 @@
"message": "Modrinth Pride"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "U heeft deze skins ontvangen omdat u tijdens Pride Month heeft gedoneerd aan een Modrinth Pride-inzamelingsactie."
"message": "Je hebt deze skins ontvangen omdat je tijdens Pride Month een donatie hebt gedaan aan een inzamelingsactie van Modrinth Pride."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
@@ -669,13 +858,13 @@
"message": "Inloggen"
},
"app.skins.sign-in.description": {
"message": "Log in op je Minecraft-account om de skinbeheerfuncties van de Modrinth-app te gebruiken."
"message": "Log in op je Minecraft-account om de functies voor het beheren van skins in de Modrinth-app te kunnen gebruiken."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Excited Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Log in"
"message": "Graag inloggen"
},
"app.skins.title": {
"message": "Skinkiezer"
@@ -734,6 +923,39 @@
"app.world.world-item.players-online": {
"message": "{count} online"
},
"content.shared-instance.change-version-body": {
"message": "Als je de versie wijzigt, wordt alleen je lokale kopie aangepast. Bij toekomstige updates van de gedeelde instantie kan deze weer worden teruggezet of opnieuw worden gewijzigd."
},
"content.shared-instance.delete-bulk-body": {
"message": "Sommige geselecteerde projecten maken deel uit van de gedeelde instantie. Als je ze verwijdert, wordt alleen je lokale kopie aangepast; bij toekomstige updates van de gedeelde instantie kunnen ze weer worden hersteld of opnieuw worden gewijzigd."
},
"content.shared-instance.delete-button": {
"message": "Toch verwijderen"
},
"content.shared-instance.delete-many-button": {
"message": "{count, plural, one {# project} other {# projecten}} toch verwijderen"
},
"content.shared-instance.delete-single-body": {
"message": "Het verwijderen past alleen je lokale kopie aan. Bij toekomstige updates van de gedeelde instantie kan deze weer worden teruggezet of opnieuw worden gewijzigd."
},
"content.shared-instance.disable-bulk-body": {
"message": "Sommige geselecteerde projecten maken deel uit van de gedeelde instantie. Als je ze uitschakelt, heeft dit alleen gevolgen voor je lokale kopie; bij toekomstige updates van de gedeelde instantie kunnen ze opnieuw worden ingeschakeld, hersteld of gewijzigd."
},
"content.shared-instance.disable-button": {
"message": "Toch uitschakelen"
},
"content.shared-instance.disable-many-button": {
"message": "{count, plural, one {# project} other {# projecten}} toch uitschakelen"
},
"content.shared-instance.disable-single-body": {
"message": "Het uitschakelen heeft alleen gevolgen voor je lokale kopie. Bij toekomstige updates van de gedeelde instantie kan deze functie opnieuw worden ingeschakeld, hersteld of gewijzigd."
},
"content.shared-instance.unlink-body": {
"message": "Het ontkoppelen heeft alleen invloed op je lokale kopie. Bij toekomstige updates van de gedeelde instantie kan deze weer worden hersteld of opnieuw worden gewijzigd."
},
"content.shared-instance.warning-header": {
"message": "Dit maakt deel uit van de gedeelde instantie"
},
"friends.action.add-friend": {
"message": "Voeg een vriend toe"
},
@@ -753,7 +975,7 @@
"message": "Voer Modrinth gebruikersnaam in..."
},
"friends.add-friend.username.title": {
"message": "Wat is de Modrinth gebruikersnaam van jouw vriend?"
"message": "Wat is de Modrinth-gebruikersnaam van je vriend(in)?"
},
"friends.add-friends-to-share": {
"message": "<link>Voeg vrienden toe</link> om te zien wat zij spelen!"
@@ -795,7 +1017,49 @@
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Log in op een Modrinth account</link> om vrienden toe te voegen en zien wat zij spelen!"
"message": "<link>Log in op een Modrinth-account</link> om vrienden toe te voegen en te zien wat zij spelen!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Gekoppelde gedeelde instantie"
},
"installation-settings.shared-instance.title": {
"message": "Instantie terugtrekken"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Gedeelde instantie ontkoppelen"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Koppel deze lokale instantie los van toekomstige gedeelde updates."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Ontkoppelen..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Gedeelde instantie terugtrekken"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Verwijder deze gedeelde instantie uit Modrinth en stuur geen updates meer naar gebruikers ervan. Je lokale instantie merkt hier geen gevolgen van."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Terugtrekken..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Dit heeft alleen gevolgen voor je lokale instantie. Je geïnstalleerde inhoud blijft op dit apparaat staan, en de gedeelde instantie en andere gebruikers ervan ondervinden hier geen hinder van."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Gedeelde instantie ontkoppelen"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Gedeelde instantie ontkoppelen"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Hiermee wordt de gedeelde instantie van de servers van Modrinth verwijderd. Gebruikers die deze instantie in de Modrinth-app gebruiken, ontvangen geen updates meer, maar je lokale instantie en de inhoud daarvan blijven op dit apparaat staan."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Gedeelde instantie terugtrekken"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Gedeelde instantie terugtrekken"
},
"instance.action.create-shortcut": {
"message": "Snelkoppeling maken"
@@ -866,6 +1130,12 @@
"instance.files.save-as": {
"message": "Opslaan als..."
},
"instance.locked.delete-button": {
"message": "Instantie verwijderen"
},
"instance.locked.play-tooltip": {
"message": "Deze instantie is vergrendeld"
},
"instance.playtime.never-played": {
"message": "Nog nooit gespeeld"
},
@@ -1004,6 +1274,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "Versie {loader}"
},
"instance.settings.tabs.installation.locked": {
"message": "Installatie-instellingen zijn niet beschikbaar zolang deze instantie is vergrendeld."
},
"instance.settings.tabs.java": {
"message": "Java en geheugen"
},
@@ -1043,6 +1316,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "//pad/naar/java"
},
"instance.settings.tabs.sharing": {
"message": "Delen"
},
"instance.settings.tabs.window": {
"message": "Venster"
},
@@ -1073,6 +1349,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "Voer breedte in..."
},
"instance.shared-instance.error.title": {
"message": "Er is iets misgegaan"
},
"instance.shared-instance.owner-tooltip": {
"message": "De inhoud van deze instantie wordt gedeeld met andere gebruikers."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Toegevoegd"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Update naar spelers verzenden"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Configuratiebestanden selecteren"
},
"instance.shared-instance.publish-review.description": {
"message": "Bekijk de inhoudelijke wijzigingen en selecteer de configuratiebestanden die je in deze update wilt opnemen."
},
"instance.shared-instance.publish-review.header": {
"message": "Wijzigingen bekijken"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Update verzenden"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Verwijderd"
},
"instance.shared-instance.tooltip": {
"message": "De inhoud van deze instantie wordt door iemand anders beheerd."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Je toegang tot de gedeelde instantie is ingetrokken. Deze instantie is nog steeds beschikbaar, maar is niet langer gekoppeld en ontvangt geen updates meer."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "De hoofdinstantie is verwijderd. Deze instantie is nog steeds beschikbaar, maar is niet meer gekoppeld en ontvangt geen updates meer."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Deze gedeelde instantie is vergrendeld door het inhoudsmoderatieteam. De instantie ontvangt geen updates meer van de hoofdinstantie en je kunt er niet meer in spelen."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instantie vergrendeld"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "de instantiebeheerder"
},
"instance.shared-instance.unavailable.text": {
"message": "Je lokale instantie is nog steeds beschikbaar, maar is niet langer gekoppeld en ontvangt geen updates meer."
},
"instance.shared-instance.unavailable.title": {
"message": "Gedeelde instantie niet meer beschikbaar"
},
"instance.worlds.a_minecraft_server": {
"message": "Een Minecraft server"
},
@@ -1125,7 +1452,7 @@
"message": "Minecraft account"
},
"minecraft-account.not-signed-in": {
"message": "Niet ingelogd"
"message": "Niet aangemeld"
},
"minecraft-account.remove-account": {
"message": "Verwijder account"
@@ -1134,7 +1461,7 @@
"message": "Selecteer account"
},
"minecraft-account.sign-in": {
"message": "Log in bij Minecraft"
"message": "Meld je aan bij Minecraft"
},
"minecraft-required.description": {
"message": "Je hebt een Microsoft-account nodig waarop Minecraft is geregistreerd voordat je het spel kunt starten en spelen."
@@ -1157,6 +1484,42 @@
"minecraft-required.sign-in": {
"message": "Aanmelden bij Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "Er is een nieuw tabblad geopend om je aan te melden. Log daar in en ga vervolgens terug naar de app."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Annuleren"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Doorgaan in je browser"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Een account aanmaken"
},
"modal.modrinth-account-required.description": {
"message": "Je moet je aanmelden bij je Modrinth-account voordat je deze functie kunt gebruiken."
},
"modal.modrinth-account-required.header": {
"message": "Account vereist"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Opnieuw de browser openen"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Bij Modrinth aanmelden"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Meld je aan bij een Modrinth-account"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Aanmelden"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Problemen bij het aanmelden? <support>Hulp krijgen</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Wachten op bevestiging van de browser..."
},
"search.filter.locked.instance": {
"message": "Aangeboden door de instantie"
},
@@ -68,18 +68,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Fargetema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Endre sida som vises når launcheren åpnes."
},
"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": "Åpningsside"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Skjuler navnelappen over din spiller på utseende siden."
},
@@ -89,9 +77,6 @@
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcheren"
},
"app.appearance-settings.select-option": {
"message": "Velg et alternativ"
},
"app.appearance-settings.show-play-time.description": {
"message": "Viser hvor lenge du har spilt et tilfelle."
},
+207 -15
View File
@@ -155,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Motyw"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ustaw stronę, która pojawia się po włączeniu launchera."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Strona główna"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Biblioteka"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Domyślna strona główna"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Wyłącza nazwę użytkownika nad modelem gracza na stronie skórek."
},
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Systemowe dekoracje"
},
"app.appearance-settings.select-option": {
"message": "Wybierz opcję"
},
"app.appearance-settings.show-play-time.description": {
"message": "Pokazuje czas gry spędzony w danej instancji."
},
@@ -359,6 +344,12 @@
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Opublikuj aktualizację"
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Sprawdź zmiany"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Trwa przegląd..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i jakakolwiek dodana zawartość."
},
@@ -407,6 +398,69 @@
"app.instance.mods.successfully-uploaded": {
"message": "Przesyłanie zakończone sukcesem"
},
"app.instance.share.empty.heading": {
"message": "Nie zaproszono żadnych znajomych"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Zaproś znajomych"
},
"app.instance.share.invite-modal.heading": {
"message": "Udostępnij {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Musisz zalogować się jako"
},
"app.instance.share.locked.empty-description-suffix": {
"message": ", by mieć dostęp do tej strony."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "połączone konto"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Nie zalogowano"
},
"app.instance.share.locked.switch-account-button": {
"message": "Zmień konto"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Złe konto"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Nie będzie w stanie otrzymywać aktualizacji dla tej udostępnionej instancji"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Co się stanie?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Odwołaj dostęp"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Odwołaj dostęp"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "Awatar użytkownika {username}"
},
"app.instance.share.sign-in.button": {
"message": "Zaloguj się"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Instaluj mimo to"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instancja"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "połączone konto"
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Zaloguj się jako"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Musisz zalogować się na Modrinth"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": ", by otrzymywać aktualizacje dla tej udostępnionej instancji."
},
"app.instance.worlds.add-server": {
"message": "Dodaj serwer"
},
@@ -458,6 +512,18 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Szukaj wśród {count} światów..."
},
"app.modal.install-to-play.additional-context": {
"message": "Dodatkowy kontekst"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "Instancja, którą zgłaszasz"
},
"app.modal.install-to-play.delete-instance": {
"message": "Usuń instancję"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Nie instaluj"
},
"app.modal.install-to-play.header": {
"message": "Zainstaluj, aby grać"
},
@@ -473,6 +539,30 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} few {# mody} other {# modów}}"
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "Plik nie jest dozwolonym formatem zdjęcia"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Plik przekracza limit rozmiaru (1MiB)"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "W przypadku powiadomień DMCA lub innych roszczeń prawnych zapoznaj się z naszą <copyright-link>Polityką dotyczącą praw autorskich</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Którą zasadę narusza ta instancja?"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Złośliwe"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Zgłoś udostępnioną instancję"
},
"app.modal.install-to-play.report-submitted": {
"message": "Zgłoszenie przesłane"
},
"app.modal.install-to-play.reviewed-files": {
"message": "Plik jest sprawdzony tylko, jeżeli został opublikowany na Modrinth, niezależnie od jego formatu (w tym pliki .mrpack)."
},
@@ -497,6 +587,21 @@
"app.modal.update-to-play.removed": {
"message": "Usunięto"
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Dodano"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Usunięto"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Ta aktualizacja udostępnionej instancji zawiera pliki, które nie zostały opublikowane na Modrinth. Zalecamy instalowanie plików tylko z źródeł, którym ufasz."
},
"app.modal.update-to-play.update-required": {
"message": "Wymagana jest aktualizacja"
},
"app.modal.update-to-play.update-required-description": {
"message": "Aktualizacja jest wymagana, by grać w {name}. Zaktualizuj do najnowszej wersji, aby uruchomić grę."
},
"app.project.install-button.already-installed": {
"message": "Ten projekt jest już zainstalowany"
},
@@ -752,6 +857,21 @@
"app.world.world-item.players-online": {
"message": "{count} online"
},
"content.shared-instance.delete-button": {
"message": "Usuń mimo to"
},
"content.shared-instance.delete-many-button": {
"message": "Usuń {count, number} projekty/ów mimo to"
},
"content.shared-instance.disable-button": {
"message": "Wyłącz mimo to"
},
"content.shared-instance.disable-many-button": {
"message": "Wyłącz {count, number} projekty/ów mimo to"
},
"content.shared-instance.warning-header": {
"message": "Jest to część udostępnionej instancji"
},
"friends.action.add-friend": {
"message": "Dodaj znajomego"
},
@@ -815,6 +935,9 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Zaloguj się na konto Modrinth</link> by dodać znajomych i widzieć, w co grają!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Powiązana udostępniona instancja"
},
"instance.action.create-shortcut": {
"message": "Utwórz skrót"
},
@@ -1061,6 +1184,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.sharing": {
"message": "Udostępnianie"
},
"instance.settings.tabs.window": {
"message": "Okno"
},
@@ -1091,12 +1217,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "Wprowadź szerokość..."
},
"instance.shared-instance.error.title": {
"message": "Coś poszło nie tak"
},
"instance.shared-instance.owner-tooltip": {
"message": "Zawartość tej instancji jest udostępniana dla innych użytkowników."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Dodano"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Opublikuj aktualizację graczom"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Wybierz pliki konfiguracji"
},
"instance.shared-instance.publish-review.description": {
"message": "Sprawdź zmiany zawartości i wybierz pliki konfiguracji, które mają być zawarte w tej aktualizacji."
},
"instance.shared-instance.publish-review.header": {
"message": "Sprawdź zmiany"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Opublikuj aktualizację"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Usunięto"
},
"instance.shared-instance.tooltip": {
"message": "Zawartość tej instancji jest zarządzana przez kogoś innego."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Twój dostęp do udostępnionej instancji został odwołany. Instancja jest nadal dostępna, ale nie jest już połączona i nie będzie otrzymywać aktualizacji."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "Instancja źródłowa została usunięta. Instancja jest nadal dostępna, ale nie jest już połączona i nie będzie otrzymywać aktualizacji."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Ta udostępniona instancja została zablokowana przez zespół moderacyjny Modrinth. Nie będzie otrzymywać dalszych aktualizacji z instancji źródłowej."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instancja zablokowana"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "zarządcę instancji"
},
"instance.shared-instance.unavailable.text": {
"message": "Twoja lokalna instancja jest nadal dostępna, ale nie jest już połączona i nie będzie otrzymywać aktualizacji."
},
"instance.shared-instance.unavailable.title": {
"message": "Udostępniona instancja nie jest już dostępna"
},
"instance.worlds.a_minecraft_server": {
"message": "Serwer Minecraft"
},
@@ -1181,6 +1352,27 @@
"minecraft-required.sign-in": {
"message": "Zaloguj się do Microsoft"
},
"modal.modrinth-account-required.cancel-button": {
"message": "Anuluj"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Kontynuuj w przeglądarce"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Utwórz konto"
},
"modal.modrinth-account-required.header": {
"message": "Konto wymagane"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Otwórz ponownie przeglądarkę"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Zaloguj się na Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Zaloguj się na konto Modrinth"
},
"search.filter.locked.instance": {
"message": "Podane przez instancję"
},
+381 -18
View File
@@ -89,6 +89,9 @@
"app.action-bar.install.unknown-instance": {
"message": "Instância desconhecida"
},
"app.action-bar.install.updating-shared-content": {
"message": "Atualizando conteúdo compartilhado"
},
"app.action-bar.installs": {
"message": "Instalações"
},
@@ -152,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Tema de cores"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Altera a página que o launcher é aberto."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Início"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Biblioteca"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página inicial padrão"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desativa o nametag acima do seu jogador na página de Skins."
},
@@ -188,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decorações nativas"
},
"app.appearance-settings.select-option": {
"message": "Selecione uma opção"
},
"app.appearance-settings.show-play-time.description": {
"message": "Exibe a quantidade de tempo jogado de uma instância."
},
@@ -201,7 +189,7 @@
"message": "Pula automaticamente as confirmações desnecessárias como duplica de instalação de pacotes de mods, exclusão de conteúdo normal, atualizações em massa, desvinculo de pacotes de mods e diálogos de reparo. Os avisos de perigo ainda serão exibidos."
},
"app.appearance-settings.skip-non-essential-warnings.title": {
"message": "Pular avisos desnecessários"
"message": "Ignorar avisos desnecessários"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Permite alternar a barra lateral."
@@ -338,6 +326,36 @@
"app.install.phase.running_loader_processors": {
"message": "Executando processadores de loader"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Adicionado"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Sua instância local é além dos usuários que você a compartilhou."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Suas alterações ainda não foram compartilhadas"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Atualização de publicação"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Publicando..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Removido"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Atualização de publicação"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Revise as alterações de conteúdo que serão compartilhadas com todos os usuários desta instância."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Revisar alterações"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "Revisando..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Todos os dados para sua instância serão excluídos permanentemente, incluindo seus mundos, configs e todo o conteúdo instalado."
},
@@ -374,6 +392,9 @@
"app.instance.mods.content-type-project": {
"message": "projeto"
},
"app.instance.mods.locked-content": {
"message": "O conteúdo em instâncias privadas não podem ser alterados."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" foi adicionado"
},
@@ -389,6 +410,102 @@
"app.instance.mods.successfully-uploaded": {
"message": "Adicionado com sucesso"
},
"app.instance.share.empty.description": {
"message": "Você pode compartilhar esta instância com seus amigos!"
},
"app.instance.share.empty.heading": {
"message": "Nenhum amigo convidado"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Convidar amigos"
},
"app.instance.share.invite-modal.heading": {
"message": "Compartilhar {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Você precisa iniciar sessão como"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "para acessar essa página."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "a conta vinculada"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Não conectado"
},
"app.instance.share.locked.switch-account-button": {
"message": "Trocar de conta"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Conta errada"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Eles não receberão mais atualizações para esta instância compartilhada"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Qualquer cópia que eles já instalaram ainda permanecerão no dispositivo deles"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Você pode convidá-los novamente depois"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Este é o último usuário, o compartilhamento será desativado para esta instância"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "O que acontece?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Revogar acesso"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Revogar acesso"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "Avatar de {username}"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Se você revogar o acesso de {username} a esta instância compartilhada, você precisará convidá-los de novo antes que possam receber atualizações."
},
"app.instance.share.sign-in.button": {
"message": "Iniciar sessão"
},
"app.instance.share.unlink.body": {
"message": "Você precisa desvincular este pacote de mods para compartilhar sua instância"
},
"app.instance.share.unlink.header": {
"message": "Para compartilhar requer desvincular"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Esta instância compartilhada já foi instalada como <bold>{instanceName}</bold>. Deseja mesmo instalar outra cópia?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Instância compartilhada já instalada"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Instalar mesmo assim"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instância"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "a conta vinculada"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "para gerenciar esta instância compartilhada. Você não conseguirá mais publicar atualizações para os usuários."
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Iniciar sessão como"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Você precisa iniciar sessão no Modrinth"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "para receber atualizações desta instância compartilhada."
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Você está usando a conta errada do Modrinth"
},
"app.instance.worlds.add-server": {
"message": "Adicionar servidor"
},
@@ -440,6 +557,21 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Buscar {count} mundos..."
},
"app.modal.install-to-play.additional-context": {
"message": "Contexto adicional"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Incluir links e imagens se possível e relevante"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "Instância que está denunciando"
},
"app.modal.install-to-play.delete-instance": {
"message": "Excluir instância"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Não instalar"
},
"app.modal.install-to-play.header": {
"message": "Instale para jogar"
},
@@ -455,12 +587,51 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, =0 {Nenhum mod} one {# mod} other {# mods}}"
},
"app.modal.install-to-play.report-description": {
"message": "Use este formulário para denunciar casos que talvez violem nossas <rules-link>Regras</rules-link> ou <terms-link>Termos de Uso</terms-link>."
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "O arquivo não é um tipo de imagem suportado"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "O arquivo excede o limite de tamanho de 1 MiB"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "Para avisos DMCA ou outras reivindicações legais, veja nossa <copyright-link>Política de Copyright</copyright-link>."
},
"app.modal.install-to-play.report-reason": {
"message": "Qual regra esta instância viola?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Inapropriado"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Malicioso"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Spam"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Denunciar instância compartilhada"
},
"app.modal.install-to-play.report-submitted": {
"message": "Denúncia enviada"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Para solicitações de suporte, entre em contato com nossa <support-link>equipe de suporte</support-link>. Para informar erros, abra uma <github-link>discussão no GitHub</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Um arquivo só é revisado se for publicado no Modrinth, independente do formato de arquivo (incluindo .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Instância compartilhada"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Conteúdo de instância compartilhado"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Esta instância compartilhada contém arquivos não publicados no Modrinth. Recomendamos fortemente a instalar somente arquivos de fontes que você confia."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Este pacote de mods do servidor contém arquivos não publicados no Modrinth. Recomendamos fortemente a instalar apenas arquivos de fontes que você confia."
},
@@ -479,6 +650,24 @@
"app.modal.update-to-play.removed": {
"message": "Removido"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Esta atualização do pacote de mods do servidor contém arquivos que não estão publicados no Modrinth. Recomendamos fortemente a apenas instalar arquivos de fontes que você confia."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Adicionado"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Removido"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Esta atualização de instância compartilhada contém arquivos não publicados no Modrinth. Recomendamos fortemente a instalar apenas de fontes que você confia."
},
"app.modal.update-to-play.update-required": {
"message": "Atualização necessária"
},
"app.modal.update-to-play.update-required-description": {
"message": "Uma atualização é necessária 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"
},
@@ -579,7 +768,7 @@
"message": "Capas"
},
"app.skins.modal.edit-title": {
"message": "Editando a skin"
"message": "Editando skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Primeiramente, faça uma edição na skin!"
@@ -624,7 +813,7 @@
"message": "Falhou ao reordenar skins"
},
"app.skins.section.builders-and-biomes": {
"message": "Construtores & Biomas"
"message": "Builders & Biomes"
},
"app.skins.section.chaos-cubed": {
"message": "Chaos Cubed"
@@ -734,6 +923,39 @@
"app.world.world-item.players-online": {
"message": "{count} ativo(s)"
},
"content.shared-instance.change-version-body": {
"message": "Alterar a versão modifica apenas a sua cópia local. Atualizações futuras da instância compartilhada podem restaurá-la ou alterá-la novamente."
},
"content.shared-instance.delete-bulk-body": {
"message": "Alguns dos projetos selecionados fazem parte da instância compartilhada. Excluí-los altera apenas sua cópia local, e futuras atualizações da instância compartilhada podem restaurá-los ou alterá-los novamente."
},
"content.shared-instance.delete-button": {
"message": "Excluir mesmo assim"
},
"content.shared-instance.delete-many-button": {
"message": "Excluir {count, plural, one {# projeto} other {# projetos}} mesmo assim"
},
"content.shared-instance.delete-single-body": {
"message": "Excluir altera apenas a sua cópia local. Atualizações futuras da instância compartilhada podem restaurá-la ou alterá-la novamente."
},
"content.shared-instance.disable-bulk-body": {
"message": "Alguns projetos selecionados fazem parte da instância compartilhada. Desativá-los afeta apenas sua cópia local, e futuras atualizações da instância compartilhada podem reativá-los, restaurá-los ou alterá-los novamente."
},
"content.shared-instance.disable-button": {
"message": "Desativar mesmo assim"
},
"content.shared-instance.disable-many-button": {
"message": "Desative {count, plural, one {# projeto} other {# projetos}} mesmo assim"
},
"content.shared-instance.disable-single-body": {
"message": "Desativá-lo altera apenas a sua cópia local. Atualizações futuras da instância compartilhada podem reativá-lo, restaurá-lo ou alterá-lo novamente."
},
"content.shared-instance.unlink-body": {
"message": "Desvincular altera apenas a sua cópia local. Atualizações futuras da instância compartilhada podem restaurá-la ou alterá-la novamente."
},
"content.shared-instance.warning-header": {
"message": "Isto faz parte da instância compartilhada"
},
"friends.action.add-friend": {
"message": "Adicionar amigo"
},
@@ -797,6 +1019,48 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Inicie uma sessão com o Modrinth</link> para adicionar amigos e ver o que eles estão jogando!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Instância compartilhada vinculada"
},
"installation-settings.shared-instance.title": {
"message": "Remover instância"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Desvincular instância compartilhada"
},
"installation-settings.shared-instance.unlink-description": {
"message": "Desconecte esta instância local de futuras atualizações compartilhadas."
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Desvinculando..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "Remover instância compartilhada"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "Remova esta instância compartilhada do Modrinth e pare de enviar atualizações para todos que a utilizam. Sua instância local não será afetada."
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "Removendo..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "Isso afeta apenas a sua instância local. O conteúdo instalado permanecerá neste dispositivo, e a instância compartilhada e outras pessoas que a utilizam não serão afetadas."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Desvinculando instância compartilhada"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Desvincular instância compartilhada"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "Isso excluirá a instância compartilhada dos servidores do Modrinth. As pessoas usando-o no Modrinth App não receberá mais atualizações, mas sua instância local e seu conteúdo ainda permanecerá no dispositivo."
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "Removendo instância compartilhada"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "Remover instância compartilhada"
},
"instance.action.create-shortcut": {
"message": "Criar atalho"
},
@@ -866,6 +1130,12 @@
"instance.files.save-as": {
"message": "Salvar como..."
},
"instance.locked.delete-button": {
"message": "Excluir instância"
},
"instance.locked.play-tooltip": {
"message": "Esta instância foi privada"
},
"instance.playtime.never-played": {
"message": "Nunca jogou"
},
@@ -1004,6 +1274,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "Versão {loader}"
},
"instance.settings.tabs.installation.locked": {
"message": "As opções de instalação estarão indisponíveis enquanto esta instância estiver privada."
},
"instance.settings.tabs.java": {
"message": "Java e memória"
},
@@ -1043,6 +1316,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/caminho/ao/java"
},
"instance.settings.tabs.sharing": {
"message": "Compartilhamento"
},
"instance.settings.tabs.window": {
"message": "Janela"
},
@@ -1073,6 +1349,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "Insira a largura..."
},
"instance.shared-instance.error.title": {
"message": "Algo deu errado"
},
"instance.shared-instance.owner-tooltip": {
"message": "O conteúdo desta instância está sendo compartilhado com outros usuários."
},
"instance.shared-instance.publish-review.added-label": {
"message": "Adicionado"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Enviar atualização aos jogadores"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Selecione os arquivos de configuração"
},
"instance.shared-instance.publish-review.description": {
"message": "Revise as alterações de conteúdo e selecione os arquivos de configuração que deseja incluir nesta atualização."
},
"instance.shared-instance.publish-review.header": {
"message": "Revisar alterações"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Atualização de publicação"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Removido"
},
"instance.shared-instance.tooltip": {
"message": "O conteúdo desta instância está sendo gerenciado por outra pessoa."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Seu acesso a esta instância compartilhada foi revogado. Ela ainda está disponível, mas não está mais vinculada e não receberá mais atualizações."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "A instância principal foi excluída. Esta instância ainda está disponível, mas não está mais vinculada e não receberá mais atualizações."
},
"instance.shared-instance.unavailable.locked-text": {
"message": "Esta instância compartilhada foi bloqueada pela equipe de Moderação de Conteúdo. Ela não receberá mais atualizações da instância principal e não poderá ser jogada."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Instância bloqueada"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "o gerenciador de instâncias"
},
"instance.shared-instance.unavailable.text": {
"message": "Sua instância local ainda está disponível, mas não está mais vinculada e não receberá atualizações."
},
"instance.shared-instance.unavailable.title": {
"message": "A instância compartilhada não está mais disponível"
},
"instance.worlds.a_minecraft_server": {
"message": "Servidor de Minecraft"
},
@@ -1157,6 +1484,42 @@
"minecraft-required.sign-in": {
"message": "Entrar na Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "Uma nova aba foi aberta para iniciar sessão. Conclua lá, então volte ao aplicativo."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Cancelar"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Continuar em seu navegador"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Criar uma conta"
},
"modal.modrinth-account-required.description": {
"message": "Você precisa iniciar sessão na sua conta do Modrinth antes de usar este recurso."
},
"modal.modrinth-account-required.header": {
"message": "Conta necessária"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Abra o navegador novamente"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Entrar no Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Iniciar sessão em uma conta do Modrinth"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Iniciando sessão"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Enfrentando problemas ao iniciar sessão? <support>Obtenha suporte</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Aguardando confirmação do navegador..."
},
"search.filter.locked.instance": {
"message": "Fornecido pela instância"
},
@@ -152,18 +152,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Cor de tema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Mudar a página em que o launcher abre."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Casa"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Livraria"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página de carregamento principal"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desativa o nametag acima do seu player na página de skins."
},
@@ -188,9 +176,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Decorações nativas"
},
"app.appearance-settings.select-option": {
"message": "Seleciona uma opção"
},
"app.appearance-settings.show-play-time.description": {
"message": "Apresenta quanto tempo passou a jogar uma instância."
},
+148 -22
View File
@@ -89,6 +89,9 @@
"app.action-bar.install.unknown-instance": {
"message": "Неизвестная сборка"
},
"app.action-bar.install.updating-shared-content": {
"message": "Обновление совместного контента"
},
"app.action-bar.installs": {
"message": "Установки"
},
@@ -152,18 +155,6 @@
"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": "Скрывать имя над игроком на странице скинов."
},
@@ -188,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Системное оформление"
},
"app.appearance-settings.select-option": {
"message": "Выберите вариант"
},
"app.appearance-settings.show-play-time.description": {
"message": "Показывать время, проведённое в сборке."
},
@@ -338,6 +326,27 @@
"app.install.phase.running_loader_processors": {
"message": "Запуск процессоров загрузчика"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Добавлен"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Локальная сборка опережает версию других участников."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Изменения не отправлены"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Отправить изменения"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Отправка..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Удалён"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Отправка изменений"
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Проверьте изменения"
},
@@ -380,6 +389,9 @@
"app.instance.mods.content-type-project": {
"message": "проект"
},
"app.instance.mods.locked-content": {
"message": "Содержимое заблокированных экземпляров нельзя изменить."
},
"app.instance.mods.project-was-added": {
"message": "«{name}» добавлен"
},
@@ -392,12 +404,30 @@
"app.instance.mods.successfully-uploaded": {
"message": "Успешно загружено"
},
"app.instance.share.empty.description": {
"message": "Поделитесь сборкой со своими друзьями!"
},
"app.instance.share.empty.heading": {
"message": "Друзья не приглашены"
"message": "Нет приглашённых друзей"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Пригласить друзей"
},
"app.instance.share.invite-modal.heading": {
"message": "Доступ к {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Необходимо войти как"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "для доступа к этой странице."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "связанный аккаунт"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Вход не выполнен"
},
"app.instance.share.locked.switch-account-button": {
"message": "Сменить аккаунт"
},
@@ -419,6 +449,12 @@
"app.instance.share.sign-in.button": {
"message": "Войти"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Сборка уже установлена"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Всё равно установить"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Сборка"
},
@@ -444,7 +480,7 @@
"message": "Удаление мира"
},
"app.instance.worlds.delete-world-modal.warning-body": {
"message": "Этот мир будет удалён навсегда из этой сборки. Отменить это действие нельзя."
"message": "Этот мир будет удалён навсегда из этой сборки. Это действие необратимо."
},
"app.instance.worlds.delete-world-modal.warning-header": {
"message": "Удаление {name}"
@@ -479,6 +515,12 @@
"app.instance.worlds.remove-server-modal.warning-header": {
"message": "Удаление {name}"
},
"app.modal.install-to-play.additional-context": {
"message": "Дополнительный контекст"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "При желании прикрепите ссылки и изображения"
},
"app.modal.install-to-play.delete-instance": {
"message": "Удалить сборку"
},
@@ -500,15 +542,39 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модов}}"
},
"app.modal.install-to-play.report-description": {
"message": "Используйте эту форму, чтобы отправить жалобу на сборки, которые могут нарушать наши <rules-link>Правила</rules-link> или <terms-link>Условия использования</terms-link>."
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Размер файла превышает лимит в 1 МиБ"
},
"app.modal.install-to-play.report-reason": {
"message": "Какое правило нарушает данная сборка?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Неприемлемое содержимое"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Вредоносное содержимое"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Спам"
},
"app.modal.install-to-play.report-submitted": {
"message": "Жалоба отправлена"
},
"app.modal.install-to-play.reviewed-files": {
"message": "Файл проверяется только если он опубликован на Modrinth, независимо от его формата (включая .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Сборка"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Контент"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Сборка содержит файлы, которые не опубликованы на Modrinth. Рекомендуется скачивать файлы только из надёжных источников."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Сборка этого сервера содержит файлы, которые не опубликованы на Modrinth. Рекомендуется скачивать файлы только из надёжных источников."
},
@@ -527,6 +593,15 @@
"app.modal.update-to-play.removed": {
"message": "Удалён"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Обновление сборки этого сервера содержит файлы, которые не опубликованы на Modrinth. Рекомендуется скачивать файлы только из надёжных источников."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Добавлен"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Удалён"
},
"app.modal.update-to-play.update-required": {
"message": "Требуется обновление"
},
@@ -797,6 +872,12 @@
"content.shared-instance.disable-button": {
"message": "Всё равно отключить"
},
"content.shared-instance.disable-many-button": {
"message": "Всё равно отключить {count, plural, one {# проект} few {# проекта} other {# проектов}}"
},
"content.shared-instance.warning-header": {
"message": "Это часть сборки"
},
"friends.action.add-friend": {
"message": "Добавить в друзья"
},
@@ -860,6 +941,21 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Войдите в аккаунт Modrinth</link>, чтобы добавлять друзей и следить за ними!"
},
"installation-settings.shared-instance.linked-title": {
"message": "Связанная сборка"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Отвязать сборку"
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Отвязка..."
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "Отвязка сборки"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "Отвязка сборки"
},
"instance.action.create-shortcut": {
"message": "Создать ярлык"
},
@@ -1106,6 +1202,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/путь/к/java"
},
"instance.settings.tabs.sharing": {
"message": "Доступ"
},
"instance.settings.tabs.window": {
"message": "Окно"
},
@@ -1136,12 +1235,30 @@
"instance.settings.tabs.window.width.enter": {
"message": "Введите ширину..."
},
"instance.shared-instance.error.title": {
"message": "Произошла ошибка"
},
"instance.shared-instance.publish-review.added-label": {
"message": "Добавлен"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Отправка изменений игрокам"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Выберите файлы настройки"
},
"instance.shared-instance.publish-review.header": {
"message": "Проверьте изменения"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Отправить изменения"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Удалён"
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Сборка заблокирована"
},
"instance.worlds.a_minecraft_server": {
"message": "Сервер Minecraft"
},
@@ -1188,19 +1305,19 @@
"message": "Мир используется"
},
"minecraft-account.add-account": {
"message": "Добавить"
"message": "Добавить аккаунт"
},
"minecraft-account.label": {
"message": "Учётная запись Minecraft"
"message": "Аккаунт Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Вход не выполнен"
},
"minecraft-account.remove-account": {
"message": "Удалить"
"message": "Удалить аккаунт"
},
"minecraft-account.select-account": {
"message": "Выберите профиль"
"message": "Выберите аккаунт"
},
"minecraft-account.sign-in": {
"message": "Войти в Minecraft"
@@ -1226,8 +1343,11 @@
"minecraft-required.sign-in": {
"message": "Войти в Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "Открылась вкладка для входа. Завершение вход и вернитесь в приложение."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Отменить"
"message": "Отмена"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Продолжите в браузере"
@@ -1235,9 +1355,15 @@
"modal.modrinth-account-required.create-account-button": {
"message": "Создать аккаунт"
},
"modal.modrinth-account-required.description": {
"message": "Войдите в аккаунт Modrinth, чтобы использовать эту функцию."
},
"modal.modrinth-account-required.header": {
"message": "Требуется аккаунт"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Открыть браузер снова"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Войти в Modrinth"
},
@@ -137,18 +137,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Tema boje"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Promeni stranicu na kojoj se otvara pokretač."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Početna stranica"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Biblioteka"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Podrazumevana odredišna stranica"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Onemogućava oznaku sa imenom iznad tvog igrača na stranici sa skinovima."
},
@@ -173,9 +161,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Zavičajni ukrasi"
},
"app.appearance-settings.select-option": {
"message": "Izaberi opciju"
},
"app.appearance-settings.show-play-time.description": {
"message": "Pokazuje koliko vremena si proveo igrajući instancu."
},
+165 -18
View File
@@ -71,6 +71,9 @@
"app.action-bar.install.unknown-instance": {
"message": "Okänd instans"
},
"app.action-bar.install.updating-shared-content": {
"message": "Uppdaterar delat innehåll"
},
"app.action-bar.installs": {
"message": "Installationer"
},
@@ -134,18 +137,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Färgtema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändrar sidan som launchern öppnas på."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Hem"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standardlandingssida"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Inaktiverar namnskylten ovanför din spelare på utseende-sidan."
},
@@ -170,9 +161,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Inbyggda fönsterdekorationer"
},
"app.appearance-settings.select-option": {
"message": "Välj ett alternativ"
},
"app.appearance-settings.show-play-time.description": {
"message": "Visar hur mycket tid du spelat på en instans."
},
@@ -320,6 +308,12 @@
"app.install.phase.running_loader_processors": {
"message": "Kör loader-processorer"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Tillagd"
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Borttagen"
},
"app.instance.confirm-delete.admonition-body": {
"message": "All data från din instans kommer permanent raderas, däribland dina världar, konfigurationer samt allt installerat innehåll."
},
@@ -371,6 +365,90 @@
"app.instance.mods.successfully-uploaded": {
"message": "Uppladdning lyckades"
},
"app.instance.share.empty.description": {
"message": "Du kan dela denna instansen med dina vänner!"
},
"app.instance.share.empty.heading": {
"message": "Inga inbjudna vänner"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Bjud in vänner"
},
"app.instance.share.invite-modal.heading": {
"message": "Dela {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Du behöver logga in som"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "för att komma åt denna sida."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "det länkade kontot"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Inte inloggad"
},
"app.instance.share.locked.switch-account-button": {
"message": "Byt konto"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Fel konto"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "De kommer inte längre få uppdateringar för denna delade instansen"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Alla kopior de redan har installerat kommer vara kvar på deras enhet"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Du kan bjuda in dem igen senare"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Vad händer?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Återkalla åtkomst"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Återkalla åtkomst"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username}s avatar"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Återkallar du {username}s åtkomst till denna delade instansen behöver du bjuda in dem igen innan de kan få uppdateringar."
},
"app.instance.share.sign-in.button": {
"message": "Logga in"
},
"app.instance.share.unlink.body": {
"message": "Du måste avlänka detta modpaket för att dela din instans"
},
"app.instance.share.unlink.header": {
"message": "Delning kräver avlänkning"
},
"app.instance.shared-instance-already-installed.body": {
"message": "Denna delade instansen är redan installerad som <bold>{instanceName}</bold>. Är du säker på att du vill installera en till kopia?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "Delad instans redan installerad"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "Installera ändå"
},
"app.instance.shared-instance-already-installed.instance": {
"message": "Instans"
},
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "det länkade kontot"
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "Logga in som"
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Du använder fel Modrinth-konto"
},
"app.instance.worlds.add-server": {
"message": "Lägg till server"
},
@@ -422,6 +500,21 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Sök {count} världar..."
},
"app.modal.install-to-play.additional-context": {
"message": "Ytterligare sammanhang"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Inkludera länkar och bilder om möjligt och relevant"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "Instans du anmäler"
},
"app.modal.install-to-play.delete-instance": {
"message": "Radera instans"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Installera inte"
},
"app.modal.install-to-play.header": {
"message": "Installera för att spela"
},
@@ -437,6 +530,9 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# moddar}}"
},
"app.modal.install-to-play.report-description": {
"message": "Använd denna formulär för att anmäla instanser som kan bryta mot våra <rules-link>Regler</rules-link> eller <terms-link>Användarvillkor</terms-link>."
},
"app.modal.install-to-play.shared-instance": {
"message": "Delad instans"
},
@@ -455,6 +551,12 @@
"app.modal.update-to-play.removed": {
"message": "Tog bort"
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Lade till"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Tog bort"
},
"app.project.install-button.already-installed": {
"message": "Detta projekt är redan installerat"
},
@@ -528,7 +630,7 @@
"message": "Fel med behandlingen av filen"
},
"app.skins.edit-button": {
"message": "Redigera utseende"
"message": "Ändra utseende"
},
"app.skins.modal.add-skin-button": {
"message": "Lägg till utseende"
@@ -552,7 +654,7 @@
"message": "Mantel"
},
"app.skins.modal.edit-title": {
"message": "Redigerar utseende"
"message": "Ändrar utseende"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Gör en ändring på utseendet först!"
@@ -579,7 +681,7 @@
"message": "Ladda upp utseende först!"
},
"app.skins.preview.edit-button": {
"message": "Redigera utseende"
"message": "Ändra utseende"
},
"app.skins.previewing-badge": {
"message": "Förhandsvisa"
@@ -764,6 +866,12 @@
"friends.sign-in-to-add-friends": {
"message": "<link>Logga in på ett Modrinth-konto</link> för att lägga till vänner och se vad de spelar!"
},
"installation-settings.shared-instance.unlink-button": {
"message": "Avlänka delad instans"
},
"installation-settings.shared-instance.unlinking-button": {
"message": "Avlänkar..."
},
"instance.action.create-shortcut": {
"message": "Skapa genväg"
},
@@ -833,6 +941,9 @@
"instance.files.save-as": {
"message": "Spara som..."
},
"instance.locked.delete-button": {
"message": "Radera instans"
},
"instance.playtime.never-played": {
"message": "Aldrig spelad"
},
@@ -1010,6 +1121,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/sökväg/till/java"
},
"instance.settings.tabs.sharing": {
"message": "Dela"
},
"instance.settings.tabs.window": {
"message": "Fönster"
},
@@ -1040,6 +1154,21 @@
"instance.settings.tabs.window.width.enter": {
"message": "Ange bredd..."
},
"instance.shared-instance.error.title": {
"message": "Något gick fel"
},
"instance.shared-instance.publish-review.added-label": {
"message": "Lade till"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Välj konfigurationsfiler"
},
"instance.shared-instance.publish-review.header": {
"message": "Granska ändringar"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Tog bort"
},
"instance.worlds.a_minecraft_server": {
"message": "En Minecraft Server"
},
@@ -1124,6 +1253,24 @@
"minecraft-required.sign-in": {
"message": "Logga in på Microsoft"
},
"modal.modrinth-account-required.cancel-button": {
"message": "Avbryt"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "Fortsätt i din webbläsare"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Skapa ett konto"
},
"modal.modrinth-account-required.header": {
"message": "Konto behövs"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Öppna webbläsare igen"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Loggar in"
},
"search.filter.locked.instance": {
"message": "Tillhandahållet av instansen"
},
+15 -15
View File
@@ -11,6 +11,21 @@
"app.action-bar.hide-more-running-instances": {
"message": "ซ่อนอินสแตนซ์ที่ทำงานอยู่เพิ่มเติม"
},
"app.action-bar.install.copied-details": {
"message": "คัดลอก"
},
"app.action-bar.install.copy-details": {
"message": "คัดลอก รายระเอียด"
},
"app.action-bar.install.dismiss": {
"message": "ปิด"
},
"app.action-bar.install.retry": {
"message": "ลองอีกครั้ง"
},
"app.action-bar.install.summary.canceled": {
"message": "ยกเลิกสำเร็จ"
},
"app.action-bar.make-primary-instance": {
"message": "กำหนดอินสแตนซ์หลัก"
},
@@ -56,18 +71,6 @@
"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": "ซ่อนป้ายชื่อบนศีรษะของผู้เล่นในหน้าแสดงสกิน"
},
@@ -92,9 +95,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "ของตกแต่งดั้งเดิม"
},
"app.appearance-settings.select-option": {
"message": "เลือกตัวเลือก"
},
"app.appearance-settings.show-play-time.description": {
"message": "แสดงเวลาที่คุณใช้เล่นในแต่ละอินสแตนซ์"
},
+82 -16
View File
@@ -89,6 +89,9 @@
"app.action-bar.install.unknown-instance": {
"message": "Bilinmeyen kurulum"
},
"app.action-bar.install.updating-shared-content": {
"message": "Paylaşılmış içerik güncelleniyor"
},
"app.action-bar.installs": {
"message": "Yüklenenler"
},
@@ -135,7 +138,7 @@
"message": "Tercihleri yönet"
},
"app.ads-consent.reject": {
"message": "Hepsini redder"
"message": "Hepsini reddet"
},
"app.ads-consent.title": {
"message": "Sizin mahremiyetiniz ve reklamlar Modrinth'i nasıl destekler"
@@ -152,18 +155,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Tema Rengi"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Launcher açıldığında varsayılan olarak açılacak sayfayı seçin."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Ana Sayfa"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Kütüphane"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Varsayılan karşılama sayfası"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Kostümler sayfasındaki karakterinizin üzerindeki isim etiketini devre dışı bırakır."
},
@@ -188,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Yerel pencere öğeleri"
},
"app.appearance-settings.select-option": {
"message": "Bir seçenek seçin"
},
"app.appearance-settings.show-play-time.description": {
"message": "Bir oyun oturumunda ne kadar zaman geçirdiğinizi gösterir."
},
@@ -338,6 +326,36 @@
"app.install.phase.running_loader_processors": {
"message": "Yükleyici işlemcileri çalıştırılıyor"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Eklendi"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Yerel instance, paylaştığın kullanıcıların kurulumundan daha güncel."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Değişiklikler henüz paylaşılmadı"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Güncellemeyi Yayınla"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Yayınlıyor..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "Kaldırıldı"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "Güncellemeyi yayınla"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "Bu kurulumu kullanan herkesle paylaşılacak içerik değişikliklerini gözden geçirin."
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "Değişimleri inceleniyor"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "İnceleniyor..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Dünyalarınız, yapılandırmalarınız ve yüklü tüm içerikler ve tüm veriler kalıcı olarak silinecek."
},
@@ -374,6 +392,9 @@
"app.instance.mods.content-type-project": {
"message": "proje"
},
"app.instance.mods.locked-content": {
"message": "İnstance'ta kilitli olan içerikler değiştirilemez."
},
"app.instance.mods.project-was-added": {
"message": "\"{name}\" eklendi"
},
@@ -389,6 +410,51 @@
"app.instance.mods.successfully-uploaded": {
"message": "Başarıyla yüklendi"
},
"app.instance.share.empty.description": {
"message": "Bu instance'ı arkadaşlarınla paylaşabilirsin!"
},
"app.instance.share.empty.heading": {
"message": "Hiçbir arkadaş davet edilmedi"
},
"app.instance.share.empty.invite-friends-button": {
"message": "Arkadaşlarını davet et"
},
"app.instance.share.invite-modal.heading": {
"message": "Paylaş {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Giriş yapmanız gerekiyor"
},
"app.instance.share.locked.empty-description-suffix": {
"message": "sayfaya giriş yapabilmeniz işin."
},
"app.instance.share.locked.linked-account-fallback": {
"message": "bağlı hesap"
},
"app.instance.share.locked.signed-out-heading": {
"message": "Giriş yapılmadı"
},
"app.instance.share.locked.switch-account-button": {
"message": "Hesap değiştir"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Hatalı hesap"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Artık bu paylaşılan kurulum için güncelleme almayacaklar"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Daha önce yükledikleri kopyalar cihazlarında kalmaya devam edecek"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Ne olur?"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "{username} nın avatarı"
},
"app.instance.share.sign-in.button": {
"message": "Giriş yap"
},
"app.instance.worlds.add-server": {
"message": "Sunucu Ekle"
},
+221 -35
View File
@@ -57,7 +57,7 @@
"message": "Недійсний шлях до файлу"
},
"app.action-bar.install.summary.invalid-modpack": {
"message": "Дані збірки модів недійсні"
"message": "Дані збірки недійсні"
},
"app.action-bar.install.summary.invalid-modpack-files": {
"message": "Файли збірки містять недійсні метадані"
@@ -66,7 +66,7 @@
"message": "Не вдалося завершити встановлення Java"
},
"app.action-bar.install.summary.loader-setup-failed": {
"message": "Не вдалося встановити завантажувач"
"message": "Помилка встановлення завантажувача"
},
"app.action-bar.install.summary.local-data-error": {
"message": "Не вдалося оновити локальні дані"
@@ -117,7 +117,7 @@
"message": "Оновити"
},
"app.action-bar.view-active-downloads": {
"message": "Переглянути активні завантаження"
"message": "Переглянути поточні завантаження"
},
"app.action-bar.view-instance": {
"message": "Переглянути профіль"
@@ -152,18 +152,6 @@
"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": "Вимикає ім'я над вашим гравцем на сторінці скінів."
},
@@ -188,14 +176,11 @@
"app.appearance-settings.native-decorations.title": {
"message": "Системне оформлення"
},
"app.appearance-settings.select-option": {
"message": "Виберіть опцію"
},
"app.appearance-settings.show-play-time.description": {
"message": "Показує, скільки часу ви провели в цьому профілі."
},
"app.appearance-settings.show-play-time.title": {
"message": "Час гри"
"message": "Показати час гри"
},
"app.appearance-settings.skip-non-essential-warnings.description": {
"message": "Автоматично пропускає низькоризикові підтвердження: установки дубльованих збірок, звичайні видалення вмісту, масові оновлення та запити на відновлення. Попередження з вищим рівнем ризику залишаться."
@@ -213,13 +198,13 @@
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж установлювати його."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Попередження перед установленням невідомих збірок"
"message": "Попереджати мене перед установленням невідомих збірок"
},
"app.auth-servers.unreachable.body": {
"message": "Сервери автентифікації Minecraft можуть зараз не працювати. Перевірте з’єднання з інтернетом та спробуйте пізніше."
},
"app.auth-servers.unreachable.header": {
"message": "Не вдається зв’язатися зі серверами автентифікації"
"message": "Не вдається зв’язатися із серверами автентифікації"
},
"app.browse.add-servers-to-instance": {
"message": "Додавання сервера до профілю"
@@ -255,7 +240,7 @@
"message": "Збірки"
},
"app.browse.server-instance-content-warning": {
"message": "Додавання вмісту може порушити сумісність при приєднанні до сервера. Усі додані матеріали також будуть утрачені під час оновлення вмісту профілю сервера."
"message": "Додавання вмісту може порушити сумісність із сервером. Будь-який доданий уміст буде втрачено під час оновлення вмісту профілю сервера."
},
"app.browse.server.installing": {
"message": "Установлення"
@@ -303,7 +288,7 @@
"message": "У черзі на встановлення"
},
"app.install.phase.preparing_java": {
"message": "Підготовка Java"
"message": "Підготовлення Java"
},
"app.install.phase.preparing_java.downloading": {
"message": "Завантаження Java {version}"
@@ -315,10 +300,10 @@
"message": "Отримання Java {version}"
},
"app.install.phase.preparing_java.resolving": {
"message": "Підготовка Java {version}"
"message": "Підготовлення Java {version}"
},
"app.install.phase.preparing_java.validating": {
"message": "Перевірка Java {version}"
"message": "Перевірення Java {version}"
},
"app.install.phase.reading_pack_manifest": {
"message": "Читання маніфесту пакета"
@@ -338,6 +323,21 @@
"app.install.phase.running_loader_processors": {
"message": "Запуск процесорів завантажувача"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "Додано"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "Ваш локальний профіль містить зміни, з якими ви не поділилися."
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "Ви ще не поділилися своїми змінами"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "Опублікувати оновлення"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "Публікуємо…"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, налаштуваннями та всім установленим умістом."
},
@@ -381,7 +381,7 @@
"message": "«{name}» додано"
},
"app.instance.mods.projects-were-added": {
"message": "{count} проєктів додано"
"message": "{count} проєктів було додано"
},
"app.instance.mods.share-text": {
"message": "Погляньте на проєкти, які я використовую у своїй збірці!"
@@ -401,12 +401,48 @@
"app.instance.share.empty.invite-friends-button": {
"message": "Запросити друзів"
},
"app.instance.share.invite-modal.heading": {
"message": "Поділитися {name}"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "Ви повинні ввійти як"
},
"app.instance.share.locked.switch-account-button": {
"message": "Змінити обліковий запис"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "Неправильний обліковий запис"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "Він більше не зможе отримувати оновлення для цього спільного профілю"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "Будь-які копії, які він уже встановив, залишаться на його пристрої"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "Ви зможете потім запросити його знову"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "Це останній користувач, тому спільний доступ буде вимкнено для цього профілю"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "Що тоді?"
},
"app.instance.share.remove-user-modal.header": {
"message": "Відкликання доступу"
},
"app.instance.share.remove-user-modal.remove-button": {
"message": "Відкликати доступ"
},
"app.instance.share.remove-user-modal.user-avatar-alt": {
"message": "Аватар {username}"
},
"app.instance.share.remove-user-modal.warning-body": {
"message": "Якщо ви відкличете доступ {username} до цього спільного профілю, він зможе отримувати оновлення, лише якщо ви знову його запросите."
},
"app.instance.share.sign-in.button": {
"message": "Увійти"
},
"app.instance.share.unlink.body": {
"message": "Ви повинні від'єднати цю збірку, аби поділитися своїм профілем"
},
@@ -425,6 +461,9 @@
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "Ви повинні ввійти в Modrinth"
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "Ви використовуєте не той обліковий запис Modrinth"
},
"app.instance.worlds.add-server": {
"message": "Додати сервер"
},
@@ -476,6 +515,21 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "Пошук {count} світів…"
},
"app.modal.install-to-play.additional-context": {
"message": "Додатковий контекст"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "Надайте посилання та зображення, які стосуються скарги"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "Профіль, на який скаржитеся"
},
"app.modal.install-to-play.delete-instance": {
"message": "Видалити профіль"
},
"app.modal.install-to-play.external-files-dont-install": {
"message": "Не встановлювати"
},
"app.modal.install-to-play.header": {
"message": "Установлення для гри"
},
@@ -491,12 +545,45 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# мод} few {# мода} other {# модів}}"
},
"app.modal.install-to-play.report-description": {
"message": "Використайте цю форму, аби поскаржитися на профілі, які можуть порушувати наші <rules-link>правила</rules-link> чи <terms-link>умови використання</terms-link>."
},
"app.modal.install-to-play.report-image-too-large": {
"message": "Файл перевищує обмеження розміру в 1 МіБ"
},
"app.modal.install-to-play.report-reason": {
"message": "Яке правило порушує цей профіль?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "Неприпустимий уміст"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "Шкідливий уміст"
},
"app.modal.install-to-play.report-reason.spam": {
"message": "Спам"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "Скарга на спільний профіль"
},
"app.modal.install-to-play.report-submitted": {
"message": "Скаргу відправлено"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "Для отримання допомоги зв'яжіться з нашою <support-link>командою підтримки</support-link>. Для створення звіту про помилки відкрийте <github-link>issue в GitHub</github-link>."
},
"app.modal.install-to-play.reviewed-files": {
"message": "Файл перевірятиметься, лише якщо його завантажено на Modrinth, незалежно від його формату (включно з .mrpack)."
},
"app.modal.install-to-play.shared-instance": {
"message": "Профіль"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "Уміст спільного профілю"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "Цей спільний профіль містить файли, які не є опубліковані на Modrinth. Ми наполегливо рекомендуємо встановлювати лише ті файли, яким довіряєте."
},
"app.modal.install-to-play.unknown-files-description": {
"message": "Ця збірка сервера містить файли, які не є опубліковані на Modrinth. Ми наполегливо рекомендуємо встановлювати лише ті файли, яким довіряєте."
},
@@ -515,6 +602,21 @@
"app.modal.update-to-play.removed": {
"message": "Видалено"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "Це оновлення збірки сервера містить файли, які не є опубліковані на Modrinth. Ми наполегливо рекомендуємо встановлювати лише ті файли, яким довіряєте."
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "Додано"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "Видалено"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "Це оновлення спільного профілю містить файли, які не є опубліковані на Modrinth. Ми наполегливо рекомендуємо встановлювати лише ті файли, яким довіряєте."
},
"app.modal.update-to-play.update-required": {
"message": "Необхідне оновлення"
},
"app.project.install-button.already-installed": {
"message": "Проєкт уже встановлено"
},
@@ -528,7 +630,7 @@
"message": "Усі версії"
},
"app.project.version.download-in-browser": {
"message": "Завантажити в браузері"
"message": "Завантажити в переглядачу"
},
"app.project.version.installing": {
"message": "Установлення"
@@ -594,7 +696,7 @@
"message": "Редагувати скін"
},
"app.skins.modal.add-skin-button": {
"message": "Додати скін"
"message": "Додати вигляд"
},
"app.skins.modal.add-title": {
"message": "Додавання скіна"
@@ -639,16 +741,16 @@
"message": "Текстура"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Спершу завантажте скін!"
"message": "Спершу завантажте вигляд!"
},
"app.skins.preview.edit-button": {
"message": "Редагувати скін"
"message": "Змінити вигляд"
},
"app.skins.previewing-badge": {
"message": "Попередній перегляд"
},
"app.skins.rate-limit.text": {
"message": "Ви змінюєте скін занадто часто. Сервери Mojang тимчасово заблокували подальші запити. Зачекайте трохи, перш ніж спробувати знову."
"message": "Ви змінюєте свій вигляд зачасто. Сервери Mojang тимчасово заблокували дальші запити. Заждіть, перш ніж спробувати знову."
},
"app.skins.rate-limit.title": {
"message": "Повільніше!"
@@ -657,7 +759,7 @@
"message": "Ваше замовлення скінів не вдалося зберегти."
},
"app.skins.reorder-error.title": {
"message": "Не вдалося змінити порядок скінів"
"message": "Не вдалося змінити порядок виглядів"
},
"app.skins.section.builders-and-biomes": {
"message": "Builders & Biomes"
@@ -669,7 +771,7 @@
"message": "Chase the Skies"
},
"app.skins.section.default-skins": {
"message": "Усталені скіни"
"message": "Усталені вигляди"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
@@ -687,7 +789,7 @@
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Збережені скіни"
"message": "Збережені вигляди"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
@@ -770,6 +872,18 @@
"app.world.world-item.players-online": {
"message": "{count} у мережі"
},
"content.shared-instance.delete-button": {
"message": "Усе одно видалити"
},
"content.shared-instance.delete-many-button": {
"message": "Усе одно видалити {count, plural, one {# проєкт} few {# проєкти} many {# проєктів} other {# проєкту}}"
},
"content.shared-instance.disable-button": {
"message": "Усе одно вимкнути"
},
"content.shared-instance.disable-many-button": {
"message": "Усе одно вимкнути {count, plural, one {# проєкт} few {# проєкти} many {# проєктів} other {# проєкту}}"
},
"friends.action.add-friend": {
"message": "Додати друга"
},
@@ -1089,7 +1203,7 @@
"message": "Повний екран"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "Запускати гру в повноекранному режимі (за допомогою options.txt)."
"message": "Змушує гру запускатися в повному екрані (за допомогою options.txt)."
},
"instance.settings.tabs.window.height": {
"message": "Висота"
@@ -1109,9 +1223,48 @@
"instance.settings.tabs.window.width.enter": {
"message": "Уведіть ширину…"
},
"instance.shared-instance.error.title": {
"message": "Щось пішло не так"
},
"instance.shared-instance.publish-review.added-label": {
"message": "Додано"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "Публікація оновлення"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "Оберіть файли конфігурації"
},
"instance.shared-instance.publish-review.description": {
"message": "Перегляньте зміни вмісту та оберіть файли конфігурації, які варто включити в цьому оновленні."
},
"instance.shared-instance.publish-review.header": {
"message": "Огляд змін"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "Опублікувати оновлення"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "Видалено"
},
"instance.shared-instance.tooltip": {
"message": "Уміст цього профілю керується іншим користувачем."
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "Ваш доступ до цього спільного профілю відкликано. Профіль досі доступний, але вже не прив'язаний та не отримуватиме оновлення."
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "Основний профіль видалено. Цей профіль досі доступний, але вже не прив'язаний та не отримуватиме оновлення."
},
"instance.shared-instance.unavailable.locked-title": {
"message": "Профіль заблоковано"
},
"instance.shared-instance.unavailable.text": {
"message": "Ваш локальний профіль досі доступний, але вже не прив'язаний та не отримуватиме оновлення."
},
"instance.shared-instance.unavailable.title": {
"message": "Спільний профіль уже не доступний"
},
"instance.worlds.a_minecraft_server": {
"message": "Сервер Minecraft"
},
@@ -1196,6 +1349,39 @@
"minecraft-required.sign-in": {
"message": "Увійти в Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "Відкрилася нова вкладка, де ви зможете ввійти. Увійдіть, тоді поверніться до застосунку."
},
"modal.modrinth-account-required.cancel-button": {
"message": "Скасувати"
},
"modal.modrinth-account-required.create-account-button": {
"message": "Створити обліковий запис"
},
"modal.modrinth-account-required.description": {
"message": "Ви повинні ввійти у свій обліковий запис Modrinth, перш ніж використовувати цей функціонал."
},
"modal.modrinth-account-required.header": {
"message": "Необхідний обліковий запис"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "Знову відкрити браузер"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "Увійти в Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "Вхід в обліковий запис Modrinth"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "Вхід"
},
"modal.modrinth-account-required.support-prompt": {
"message": "Не можете ввійти? <support>Отримати допомогу</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "Очікуємо на підтвердження від переглядача…"
},
"search.filter.locked.instance": {
"message": "Надано профілем"
},
@@ -146,18 +146,6 @@
"app.appearance-settings.color-theme.title": {
"message": "Màu chủ đề"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Thay đổi trang hiển thị khi khởi chạy launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Trang chủ"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Thư viện"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Trang mặc định"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Tắt hiển thị thẻ tên phía trên người chơi ở trang skin."
},
@@ -182,9 +170,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "Giao diện cửa sổ hệ thống"
},
"app.appearance-settings.select-option": {
"message": "Chọn một tuỳ chọn"
},
"app.appearance-settings.show-play-time.description": {
"message": "Hiển thị thời gian bạn đã dành để chơi bằng một cấu hình."
},
+22 -37
View File
@@ -18,7 +18,7 @@
"message": "复制详细信息"
},
"app.action-bar.install.dismiss": {
"message": "忽略"
"message": "隐藏"
},
"app.action-bar.install.open-instance": {
"message": "打开实例"
@@ -30,19 +30,19 @@
"message": "由于应用关闭而取消"
},
"app.action-bar.install.summary.bad-modpack-file": {
"message": "整合包读取失败"
"message": "无法读取整合包"
},
"app.action-bar.install.summary.canceled": {
"message": "已取消"
},
"app.action-bar.install.summary.cleanup-incomplete": {
"message": "未完成清理"
"message": "未完成清理"
},
"app.action-bar.install.summary.content-download-failed": {
"message": "无法下载文件"
},
"app.action-bar.install.summary.corrupt-download": {
"message": "下载的文件损坏"
"message": "下载的文件损坏"
},
"app.action-bar.install.summary.could-not-save-files": {
"message": "无法保存文件"
@@ -60,7 +60,7 @@
"message": "整合包数据无效"
},
"app.action-bar.install.summary.invalid-modpack-files": {
"message": "整合包文件含有无效元数据"
"message": "整合包文件含有无效元数据"
},
"app.action-bar.install.summary.java-setup-failed": {
"message": "无法完成 Java 配置"
@@ -69,22 +69,22 @@
"message": "加载器配置失败"
},
"app.action-bar.install.summary.local-data-error": {
"message": "无法更新本地数据"
"message": "无法更新本地数据"
},
"app.action-bar.install.summary.minecraft-setup-failed": {
"message": "Minecraft 配置失败"
},
"app.action-bar.install.summary.modrinth-unreachable": {
"message": "无法连接 Modrinth"
"message": "无法连接Modrinth"
},
"app.action-bar.install.summary.no-write-permission": {
"message": "没有写入权限"
"message": "没有写入权限"
},
"app.action-bar.install.summary.pack-download-failed": {
"message": "无法下载包"
"message": "无法下载整合包"
},
"app.action-bar.install.summary.unexpected-error": {
"message": "出错了"
"message": "出现了一些问题"
},
"app.action-bar.install.unknown-instance": {
"message": "未知的实例"
@@ -155,18 +155,6 @@
"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": "在皮肤页面中禁用你玩家头顶上的名牌。"
},
@@ -191,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "原生窗口"
},
"app.appearance-settings.select-option": {
"message": "选择一个选项"
},
"app.appearance-settings.show-play-time.description": {
"message": "显示你在每个实例中游玩的时间。"
},
@@ -225,7 +210,7 @@
"message": "无法连接到身份验证服务器"
},
"app.browse.add-servers-to-instance": {
"message": "添加服务器到实例"
"message": "添加实例到服务器"
},
"app.browse.add-to-an-instance": {
"message": "添加到实例"
@@ -297,7 +282,7 @@
"message": "正在下载包文件"
},
"app.install.phase.extracting_overrides": {
"message": "提取覆盖项"
"message": "正在解压覆盖项"
},
"app.install.phase.finalizing": {
"message": "完成"
@@ -312,7 +297,7 @@
"message": "正在下载 Java {version}"
},
"app.install.phase.preparing_java.extracting": {
"message": "正在提取 Java {version}"
"message": "正在解压 Java {version}"
},
"app.install.phase.preparing_java.fetching-metadata": {
"message": "正在获取 Java {version}"
@@ -336,7 +321,7 @@
"message": "正在解析内容"
},
"app.install.phase.rolling_back": {
"message": "回滚"
"message": "回滚"
},
"app.install.phase.running_loader_processors": {
"message": "正在运行加载处理器"
@@ -453,7 +438,7 @@
"message": "切换账号"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "账号不正确"
"message": "账号错误"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "对方将不再收到该共享实例的后续更新"
@@ -540,7 +525,7 @@
"message": "正在删除 {name}"
},
"app.instance.worlds.filter-modded": {
"message": "模组"
"message": "适配模组"
},
"app.instance.worlds.filter-offline": {
"message": "离线"
@@ -945,10 +930,10 @@
"message": "部分选中的项目属于共享实例的一部分。删除它们只会修改你的本地副本,未来的共享实例更新可能会再次将其恢复或更改。"
},
"content.shared-instance.delete-button": {
"message": "要删除"
"message": "要删除"
},
"content.shared-instance.delete-many-button": {
"message": "要删除{count, number}"
"message": "要删除 {count, number} 个项目"
},
"content.shared-instance.delete-single-body": {
"message": "删除它只会修改你的本地副本。未来的共享实例更新可能会再次将其恢复或更改。"
@@ -957,10 +942,10 @@
"message": "部分选中的项目属于共享实例的一部分。禁用它们只会修改你的本地副本,未来的共享实例更新可能会再次重新启用、恢复或更改它们。"
},
"content.shared-instance.disable-button": {
"message": "坚持禁用"
"message": "仍要禁用"
},
"content.shared-instance.disable-many-button": {
"message": "坚持禁用 {count, number} 个项目"
"message": "仍要禁用 {count, number} 个项目"
},
"content.shared-instance.disable-single-body": {
"message": "禁用它只会修改你的本地副本。未来的共享实例更新可能会再次将其启用、恢复或更改。"
@@ -1209,7 +1194,7 @@
"message": "库分组"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "创建新的分组"
"message": "新建分组"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "库分组功能可以让你将实例整理到库中的不同部分。"
@@ -1509,7 +1494,7 @@
"message": "请在你的浏览器中继续"
},
"modal.modrinth-account-required.create-account-button": {
"message": "建账户"
"message": "建账户"
},
"modal.modrinth-account-required.description": {
"message": "在使用这个功能之前,你需要登入你的Modrinth账户。"
+296 -17
View File
@@ -89,6 +89,9 @@
"app.action-bar.install.unknown-instance": {
"message": "未知的實例"
},
"app.action-bar.install.updating-shared-content": {
"message": "正在更新共用內容"
},
"app.action-bar.installs": {
"message": "安裝"
},
@@ -152,18 +155,6 @@
"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": "在外觀頁面中隱藏玩家上方的名牌。"
},
@@ -188,9 +179,6 @@
"app.appearance-settings.native-decorations.title": {
"message": "原生視窗"
},
"app.appearance-settings.select-option": {
"message": "選擇選項"
},
"app.appearance-settings.show-play-time.description": {
"message": "顯示你在該實例的遊玩時數。"
},
@@ -338,6 +326,36 @@
"app.install.phase.running_loader_processors": {
"message": "正在執行載入器處理程式"
},
"app.instance.admonitions.shared-instance.added-label": {
"message": "已新增"
},
"app.instance.admonitions.shared-instance.changes-body": {
"message": "你的本機實例有尚未同步給其他使用者的變更。"
},
"app.instance.admonitions.shared-instance.changes-header": {
"message": "變更尚未共用"
},
"app.instance.admonitions.shared-instance.publish-button": {
"message": "推送更新"
},
"app.instance.admonitions.shared-instance.publishing-button": {
"message": "正在推送..."
},
"app.instance.admonitions.shared-instance.removed-label": {
"message": "已移除"
},
"app.instance.admonitions.shared-instance.review-admonition-header": {
"message": "推送更新"
},
"app.instance.admonitions.shared-instance.review-description": {
"message": "審查即將與這個實例的所有使用者共用的內容變更。"
},
"app.instance.admonitions.shared-instance.review-header": {
"message": "審查變更"
},
"app.instance.admonitions.shared-instance.reviewing-button": {
"message": "正在審查..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "你實例中的所有資料將被永久刪除,包含你的世界、設定檔以及所有已安裝的內容。"
},
@@ -374,6 +392,9 @@
"app.instance.mods.content-type-project": {
"message": "專案"
},
"app.instance.mods.locked-content": {
"message": "已鎖定實例中的內容無法變更。"
},
"app.instance.mods.project-was-added": {
"message": "已新增「{name}」"
},
@@ -389,9 +410,18 @@
"app.instance.mods.successfully-uploaded": {
"message": "已成功上傳"
},
"app.instance.share.empty.description": {
"message": "你可以與好友共用這個實例!"
},
"app.instance.share.empty.heading": {
"message": "未邀請任何好友"
},
"app.instance.share.empty.invite-friends-button": {
"message": "邀請好友"
},
"app.instance.share.invite-modal.heading": {
"message": "共用「{name}」"
},
"app.instance.share.locked.empty-description-prefix": {
"message": "你需要以"
},
@@ -401,6 +431,27 @@
"app.instance.share.locked.linked-account-fallback": {
"message": "已連結的帳號"
},
"app.instance.share.locked.signed-out-heading": {
"message": "未登入"
},
"app.instance.share.locked.switch-account-button": {
"message": "切換帳號"
},
"app.instance.share.locked.wrong-account-heading": {
"message": "帳號錯誤"
},
"app.instance.share.remove-user-modal.effect-access": {
"message": "他不會再收到這個共用實例的更新"
},
"app.instance.share.remove-user-modal.effect-installed-copy": {
"message": "他已經安裝的副本仍會保留在他的裝置上"
},
"app.instance.share.remove-user-modal.effect-invite-again": {
"message": "你可以稍後再次邀請他"
},
"app.instance.share.remove-user-modal.effect-last-user": {
"message": "這是最後一位共用使用者,這個實例的共用功能將被關閉"
},
"app.instance.share.remove-user-modal.effects-label": {
"message": "會發生什麼事?"
},
@@ -419,6 +470,18 @@
"app.instance.share.sign-in.button": {
"message": "登入"
},
"app.instance.share.unlink.body": {
"message": "你必須先與模組包取消連結才能共用實例"
},
"app.instance.share.unlink.header": {
"message": "共用前需要取消連結"
},
"app.instance.shared-instance-already-installed.body": {
"message": "這個共用實例已經在本機安裝,名稱為「<bold>{instanceName}</bold>」。你確定要再安裝一個副本嗎?"
},
"app.instance.shared-instance-already-installed.header": {
"message": "共用實例已經安裝"
},
"app.instance.shared-instance-already-installed.install-anyway": {
"message": "仍要安裝"
},
@@ -428,9 +491,21 @@
"app.instance.shared-instance-wrong-account.fallback-username": {
"message": "已連結的帳號"
},
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
"message": "登入才能管理這個共用實例,否則你將無法向使用者推送更新。"
},
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
"message": "你需要以"
},
"app.instance.shared-instance-wrong-account.signed-out-header": {
"message": "你需要登入 Modrinth"
},
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
"message": "登入才能收到這個共用實例的更新。"
},
"app.instance.shared-instance-wrong-account.warning-header": {
"message": "你目前使用的 Modrinth 帳號不正確"
},
"app.instance.worlds.add-server": {
"message": "新增伺服器"
},
@@ -450,7 +525,7 @@
"message": "即將刪除「{name}」"
},
"app.instance.worlds.filter-modded": {
"message": "模組"
"message": "模組"
},
"app.instance.worlds.filter-offline": {
"message": "離線"
@@ -482,6 +557,15 @@
"app.instance.worlds.search-worlds-placeholder": {
"message": "搜尋 {count} 個世界..."
},
"app.modal.install-to-play.additional-context": {
"message": "補充說明"
},
"app.modal.install-to-play.additional-context-placeholder": {
"message": "若可行且相關,請一併附上連結與圖片"
},
"app.modal.install-to-play.content-you-are-reporting": {
"message": "你要檢舉的實例"
},
"app.modal.install-to-play.delete-instance": {
"message": "刪除實例"
},
@@ -503,14 +587,23 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# 個模組}}"
},
"app.modal.install-to-play.report-description": {
"message": "用這個表單檢舉可能違反我們的《<rules-link>內容規範</rules-link>》或《<terms-link>使用條款</terms-link>》的實例。"
},
"app.modal.install-to-play.report-image-invalid-type": {
"message": "不支援的圖片格式"
},
"app.modal.install-to-play.report-image-too-large": {
"message": "檔案大小超過 1 MiB 限制"
},
"app.modal.install-to-play.report-legal-claims": {
"message": "關於 DMCA 通知或其他法律訴求,請參閱我們的《<copyright-link>著作權政策</copyright-link>》。"
},
"app.modal.install-to-play.report-reason": {
"message": "這個實例違反了哪一條規則?"
},
"app.modal.install-to-play.report-reason.inappropriate": {
"message": "不當內容"
"message": "不當內容"
},
"app.modal.install-to-play.report-reason.malicious": {
"message": "惡意內容"
@@ -518,12 +611,27 @@
"app.modal.install-to-play.report-reason.spam": {
"message": "垃圾內容"
},
"app.modal.install-to-play.report-shared-instance-header": {
"message": "檢舉共用實例"
},
"app.modal.install-to-play.report-submitted": {
"message": "檢舉已提交"
},
"app.modal.install-to-play.report-support-and-bugs": {
"message": "如需客服支援,請聯絡我們的<support-link>客服團隊</support-link>。如需回報錯誤,請<github-link>在 GitHub 上提交 issue</github-link>。"
},
"app.modal.install-to-play.reviewed-files": {
"message": "只有在 Modrinth 上發布的檔案才會經過審查,無論其檔案格式為何(包含 .mrpack)。"
},
"app.modal.install-to-play.shared-instance": {
"message": "共用實例"
},
"app.modal.install-to-play.shared-instance-content": {
"message": "共用實例的內容"
},
"app.modal.install-to-play.shared-instance-unknown-files-description": {
"message": "這個共用實例包含未在 Modrinth 上發布的檔案。強烈建議你僅安裝來自信任來源的檔案。"
},
"app.modal.install-to-play.unknown-files-description": {
"message": "這個伺服器模組包中包含未在 Modrinth 上發布的檔案。強烈建議你僅安裝來自信任來源的檔案。"
},
@@ -542,6 +650,21 @@
"app.modal.update-to-play.removed": {
"message": "已移除"
},
"app.modal.update-to-play.server-modpack-unknown-files-description": {
"message": "這個伺服器模組包更新包含未在 Modrinth 上發布的檔案。強烈建議你僅安裝來自信任來源的檔案。"
},
"app.modal.update-to-play.shared-instance-added-label": {
"message": "已新增"
},
"app.modal.update-to-play.shared-instance-removed-label": {
"message": "已移除"
},
"app.modal.update-to-play.shared-instance-unknown-files-description": {
"message": "這個共用實例更新包含未在 Modrinth 上發布的檔案。強烈建議你僅安裝來自信任來源的檔案。"
},
"app.modal.update-to-play.update-required": {
"message": "需要更新"
},
"app.modal.update-to-play.update-required-description": {
"message": "需要更新才能遊玩「{name}」。請更新至最新版本以啟動遊戲。"
},
@@ -800,9 +923,39 @@
"app.world.world-item.players-online": {
"message": "{count} 人在線上"
},
"content.shared-instance.change-version-body": {
"message": "變更版本只會影響本機副本。未來共用實例更新時可能會再次復原或變更這個專案。"
},
"content.shared-instance.delete-bulk-body": {
"message": "部分選取的專案是共用實例的一部分。刪除這些專案只會影響你的本機副本,而未來共用實例更新時可能會還原或再次變更這些專案。"
},
"content.shared-instance.delete-button": {
"message": "仍要刪除"
},
"content.shared-instance.delete-many-button": {
"message": "仍要刪除 {count, number} 個專案"
},
"content.shared-instance.delete-single-body": {
"message": "刪除這個專案只會影響本機副本。未來共用實例更新時可能會再次復原或變更這個專案。"
},
"content.shared-instance.disable-bulk-body": {
"message": "部分選取的專案是共用實例的一部分。停用這些專案只會影響你的本機副本,而未來共用實例更新時可能會重新啟用、還原或再次變更這些專案。"
},
"content.shared-instance.disable-button": {
"message": "仍要停用"
},
"content.shared-instance.disable-many-button": {
"message": "仍要停用 {count, number} 個專案"
},
"content.shared-instance.disable-single-body": {
"message": "停用這個專案只會影響本機副本。未來共用實例更新時,可能會重新啟用、還原或再次變更這個專案。"
},
"content.shared-instance.unlink-body": {
"message": "解除連結只會影響本機副本。未來共用實例更新時可能會復原或再次變更。"
},
"content.shared-instance.warning-header": {
"message": "這是共用實例的一部分"
},
"friends.action.add-friend": {
"message": "新增好友"
},
@@ -866,6 +1019,48 @@
"friends.sign-in-to-add-friends": {
"message": "<link>登入 Modrinth 帳號</link>即可新增好友並查看他們正在玩什麼!"
},
"installation-settings.shared-instance.linked-title": {
"message": "已連結的共用實例"
},
"installation-settings.shared-instance.title": {
"message": "取消發布實例"
},
"installation-settings.shared-instance.unlink-button": {
"message": "取消連結共用實例"
},
"installation-settings.shared-instance.unlink-description": {
"message": "解除這個本機實例與後續共用更新的連結。"
},
"installation-settings.shared-instance.unlinking-button": {
"message": "正在取消連結..."
},
"installation-settings.shared-instance.unpublish-button": {
"message": "取消發布共用實例"
},
"installation-settings.shared-instance.unpublish-description": {
"message": "在 Modrinth 上移除這個共用實例,並停止向任何使用者推送更新。你的本機實例不會受到影響。"
},
"installation-settings.shared-instance.unpublishing-button": {
"message": "正在取消發布..."
},
"installation-settings.unlink-shared-instance.modal.admonition-body": {
"message": "這只會影響你的本機實例。你已安裝的內容仍會保留在此裝置上,共用實例及其他使用者都不會受到影響。"
},
"installation-settings.unlink-shared-instance.modal.admonition-header": {
"message": "即將取消連結共用實例"
},
"installation-settings.unlink-shared-instance.modal.header": {
"message": "取消連結共用實例"
},
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
"message": "這會從 Modrinth 的伺服器刪除共用實例。在 Modrinth App 使用該實例的使用者將不再收到更新,但本機實例及其內容仍會保留在此裝置上。"
},
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
"message": "即將取消發布共用實例"
},
"installation-settings.unpublish-shared-instance.modal.header": {
"message": "取消發布共用實例"
},
"instance.action.create-shortcut": {
"message": "建立捷徑"
},
@@ -938,6 +1133,9 @@
"instance.locked.delete-button": {
"message": "刪除實例"
},
"instance.locked.play-tooltip": {
"message": "這個實例已被鎖定"
},
"instance.playtime.never-played": {
"message": "從未遊玩"
},
@@ -1076,6 +1274,9 @@
"instance.settings.tabs.installation.loader-version": {
"message": "{loader} 版本"
},
"instance.settings.tabs.installation.locked": {
"message": "實例被鎖定時無法使用安裝設定選項。"
},
"instance.settings.tabs.java": {
"message": "Java 和記憶體"
},
@@ -1115,6 +1316,9 @@
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.sharing": {
"message": "共用"
},
"instance.settings.tabs.window": {
"message": "視窗"
},
@@ -1145,6 +1349,57 @@
"instance.settings.tabs.window.width.enter": {
"message": "輸入寬度..."
},
"instance.shared-instance.error.title": {
"message": "出了點狀況"
},
"instance.shared-instance.owner-tooltip": {
"message": "這個實例的內容正在與其他使用者共用。"
},
"instance.shared-instance.publish-review.added-label": {
"message": "已新增"
},
"instance.shared-instance.publish-review.admonition-header": {
"message": "向玩家推送更新"
},
"instance.shared-instance.publish-review.config-title-v2": {
"message": "選擇設定檔"
},
"instance.shared-instance.publish-review.description": {
"message": "審查內容變更,並選擇要包含在本次更新的設定檔。"
},
"instance.shared-instance.publish-review.header": {
"message": "審查變更"
},
"instance.shared-instance.publish-review.publish-button": {
"message": "推送更新"
},
"instance.shared-instance.publish-review.removed-label": {
"message": "已移除"
},
"instance.shared-instance.tooltip": {
"message": "這個實例的內容由其他使用者負責管理。"
},
"instance.shared-instance.unavailable.access-revoked-text": {
"message": "你對共用實例的存取權已被撤銷。這個實例仍可正常使用,但已不再與共用實例連結,且不再接收更新。"
},
"instance.shared-instance.unavailable.deleted-text": {
"message": "主要實例已被刪除。這個實例仍可正常使用,但已不再與共用實例連結,且不再接收更新。"
},
"instance.shared-instance.unavailable.locked-text": {
"message": "這個共用實例已被內容管理團隊鎖定,不會再接收來自主要實例的更新,且無法遊玩。"
},
"instance.shared-instance.unavailable.locked-title": {
"message": "實例已鎖定"
},
"instance.shared-instance.unavailable.manager-fallback": {
"message": "實例管理者"
},
"instance.shared-instance.unavailable.text": {
"message": "你的本機實例仍可正常使用,但已不再與共用實例連結,且不再接收更新。"
},
"instance.shared-instance.unavailable.title": {
"message": "你無法使用這個共用實例"
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft 伺服器"
},
@@ -1229,18 +1484,42 @@
"minecraft-required.sign-in": {
"message": "登入 Microsoft"
},
"modal.modrinth-account-required.browser-description": {
"message": "一個新分頁已開啟供你登入。請在該分頁完成登入,然後返回應用程式。"
},
"modal.modrinth-account-required.cancel-button": {
"message": "取消"
},
"modal.modrinth-account-required.continue-in-browser-heading": {
"message": "在瀏覽器中繼續"
},
"modal.modrinth-account-required.create-account-button": {
"message": "建立帳號"
},
"modal.modrinth-account-required.description": {
"message": "你必須登入 Modrinth 帳號才能使用這個功能。"
},
"modal.modrinth-account-required.header": {
"message": "需要帳號"
},
"modal.modrinth-account-required.open-browser-again-button": {
"message": "再次開啟瀏覽器"
},
"modal.modrinth-account-required.sign-in-button": {
"message": "登入 Modrinth"
},
"modal.modrinth-account-required.sign-in-heading": {
"message": "登入 Modrinth 帳號"
},
"modal.modrinth-account-required.signing-in-header": {
"message": "登入"
},
"modal.modrinth-account-required.support-prompt": {
"message": "登入時遇到問題嗎?<support>取得支援</support>"
},
"modal.modrinth-account-required.waiting-for-browser": {
"message": "等待瀏覽器確認..."
},
"search.filter.locked.instance": {
"message": "由該實例提供"
},
+4 -2
View File
@@ -975,8 +975,10 @@ const searchState = useBrowseSearch({
watch(
[
() => searchState.query.value,
() => searchState.currentFilters.value,
() => searchState.serverCurrentFilters.value,
() =>
searchState.isServerType.value
? searchState.serverCurrentFilters.value
: searchState.currentFilters.value,
() => projectType.value,
],
() => {
+15 -85
View File
@@ -1,19 +1,20 @@
<script setup lang="ts">
import { injectNotificationManager } from '@modrinth/ui'
import type { SearchResult } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import LibrarySection from '@/components/ui/library/index.vue'
import WelcomeScreen from '@/components/ui/WelcomeScreen.vue'
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
import { get_search_results } from '@/helpers/cache.js'
import { instance_listener } from '@/helpers/events'
import { list } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { injectOnboardingChecklist } from '@/providers/onboarding-checklist'
import { useBreadcrumbs } from '@/store/breadcrumbs'
const { handleError } = injectNotificationManager()
const { hasCreatedInstance, isReady } = injectOnboardingChecklist()
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
@@ -21,10 +22,6 @@ breadcrumbs.setRootContext({ name: 'Home', link: route.path })
const instances = ref<GameInstance[]>([])
const featuredModpacks = ref<SearchResult[]>([])
const featuredMods = ref<SearchResult[]>([])
const installedModpacksFilter = ref('')
const recentInstances = computed(() =>
instances.value
.filter((x) => x.last_played)
@@ -32,68 +29,19 @@ const recentInstances = computed(() =>
.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played))),
)
const hasFeaturedProjects = computed(
() => (featuredModpacks.value?.length ?? 0) + (featuredMods.value?.length ?? 0) > 0,
)
const offline = ref<boolean>(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
async function fetchInstances() {
instances.value = await list().catch(handleError)
const filters = []
for (const instance of instances.value) {
if (instance.link && instance.link.project_id) {
filters.push(`NOT"project_id"="${instance.link.project_id}"`)
}
}
installedModpacksFilter.value = filters.join(' AND ')
}
async function fetchFeaturedModpacks() {
const response = await get_search_results(
`?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${installedModpacksFilter.value}`,
)
if (response) {
featuredModpacks.value = response.result.hits
} else {
featuredModpacks.value = []
try {
instances.value = await list()
} catch (error: unknown) {
handleError(error)
}
}
async function fetchFeaturedMods() {
const response = await get_search_results('?facets=[["project_type:mod"]]&limit=10&index=follows')
if (response) {
featuredMods.value = response.result.hits
} else {
featuredModpacks.value = []
}
if (hasCreatedInstance.value) {
await fetchInstances()
}
async function refreshFeaturedProjects() {
await Promise.all([fetchFeaturedModpacks(), fetchFeaturedMods()])
}
await fetchInstances()
await refreshFeaturedProjects()
const unlistenInstance = await instance_listener(
async (e: { event: string; instance_id: string }) => {
await fetchInstances()
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
await refreshFeaturedProjects()
}
},
)
const unlistenInstance = await instance_listener(fetchInstances)
onUnmounted(() => {
unlistenInstance()
@@ -101,27 +49,9 @@ onUnmounted(() => {
</script>
<template>
<div class="p-6 flex flex-col gap-2">
<h1 v-if="recentInstances?.length > 0" class="m-0 text-2xl font-extrabold">Welcome back!</h1>
<h1 v-else class="m-0 text-2xl font-extrabold">Welcome to Modrinth App!</h1>
<RecentWorldsList :recent-instances="recentInstances" />
<RowDisplay
v-if="hasFeaturedProjects"
:instances="[
{
label: 'Discover a modpack',
route: '/browse/modpack',
instances: featuredModpacks,
downloaded: false,
},
{
label: 'Discover mods',
route: '/browse/mod',
instances: featuredMods,
downloaded: false,
},
]"
:can-paginate="true"
/>
<WelcomeScreen v-if="isReady && !hasCreatedInstance" />
<div v-else-if="isReady" class="flex flex-col gap-6 p-6">
<RecentWorldsList v-if="recentInstances?.length > 0" :recent-instances="recentInstances" />
<LibrarySection :instances="instances" />
</div>
</template>
+108
View File
@@ -0,0 +1,108 @@
<template>
<div class="w-full pt-2">
<UserProfilePageLayout
:user-id="userId"
:project-type="projectType"
variant="app"
site-url="https://modrinth.com"
project-link-mode="app"
external-navigation
/>
</div>
</template>
<script setup lang="ts">
import { provideUserProfile, UserProfilePageLayout } from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, watch } from 'vue'
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
import {
get_user_collections,
get_user_organizations,
get_user_profile,
get_user_projects,
patch_user,
} from '@/helpers/users'
import { useBreadcrumbs } from '@/store/breadcrumbs'
const route = useRoute()
const queryClient = useQueryClient()
const breadcrumbs = useBreadcrumbs()
const userProfile = provideUserProfile({
getUser: get_user_profile,
getProjects: get_user_projects,
getOrganizations: get_user_organizations,
getCollections: get_user_collections,
patchUser: patch_user,
})
const userId = computed(() => {
const value = route.params.user
return Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
})
const projectType = computed(() => {
const value = route.params.projectType
return Array.isArray(value) ? value[0] : value
})
async function ensureUserProfileData(id: string): Promise<void> {
if (!id) return
let breadcrumbName = id
try {
const user = await queryClient.ensureQueryData({
queryKey: ['user', id],
queryFn: () => userProfile.getUser(id),
staleTime: 30_000,
})
breadcrumbName = user.username
} catch {
// Let the mounted layout's useQuery surface errors; do not fail route setup.
}
await Promise.allSettled([
queryClient.ensureQueryData({
queryKey: ['user', id, 'projects'],
queryFn: () => userProfile.getProjects(id),
staleTime: 30_000,
}),
queryClient.ensureQueryData({
queryKey: ['user', id, 'organizations'],
queryFn: () => userProfile.getOrganizations(id),
staleTime: 30_000,
}),
queryClient.ensureQueryData({
queryKey: ['user', id, 'collections'],
queryFn: () => userProfile.getCollections(id),
staleTime: 30_000,
}),
])
breadcrumbs.setName('User', breadcrumbName)
}
onBeforeRouteUpdate(async (to) => {
const value = to.params.user
const id = Array.isArray(value) ? (value[0] ?? '') : (value ?? '')
await ensureUserProfileData(id)
})
breadcrumbs.setName('User', userId.value)
await ensureUserProfileData(userId.value)
const { data: user } = useQuery({
queryKey: computed(() => ['user', userId.value]),
queryFn: () => userProfile.getUser(userId.value),
enabled: false,
staleTime: 30_000,
})
watch(
[userId, user],
([currentUserId, value]) => {
breadcrumbs.setName('User', value?.username ?? currentUserId)
},
{ immediate: true },
)
</script>
@@ -5,7 +5,6 @@ import {
ServersManageAccessPage,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
const client = injectModrinthClient()
const { serverId } = injectModrinthServerContext()
@@ -29,7 +28,7 @@ try {
}
function userProfileLink(username: string) {
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
return `/user/${encodeURIComponent(username)}`
}
</script>
+2 -1
View File
@@ -2,6 +2,7 @@ import Browse from './Browse.vue'
import Index from './Index.vue'
import Servers from './Servers.vue'
import Skins from './Skins.vue'
import User from './User.vue'
import Worlds from './Worlds.vue'
export { Browse, Index, Servers, Skins, Worlds }
export { Browse, Index, Servers, Skins, User, Worlds }
@@ -198,6 +198,13 @@ const messages = defineMessages({
let savedModalState: ModpackContentModalState | null = null
function contentOwnerLink(owner: ContentOwner): NonNullable<ContentOwner['link']> {
if (owner.type === 'user') return `/user/${encodeURIComponent(owner.id)}`
return () => {
void openUrl(`https://modrinth.com/organization/${owner.id}`)
}
}
const { formatMessage } = useVIntl()
const { handleError, addNotification } = injectNotificationManager()
const { installingItems, installRevisionByInstance, installFailureRevisionByInstance } =
@@ -1390,10 +1397,7 @@ provideContentManager({
owner: linkedModpackOwner.value
? {
...linkedModpackOwner.value,
link: () =>
openUrl(
`https://modrinth.com/${linkedModpackOwner.value!.type}/${linkedModpackOwner.value!.id}`,
),
link: contentOwnerLink(linkedModpackOwner.value),
}
: undefined,
categories: linkedModpackCategories.value,
@@ -1491,7 +1495,7 @@ provideContentManager({
owner: item.owner
? {
...item.owner,
link: () => openUrl(`https://modrinth.com/${item.owner!.type}/${item.owner!.id}`),
link: contentOwnerLink(item.owner),
}
: undefined,
enabled: canMutateContent(item) ? item.enabled : undefined,
@@ -138,7 +138,6 @@ import {
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref, toRef, watch } from 'vue'
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
@@ -342,9 +341,7 @@ function removeMember(row: ShareRow) {
members.remove(row.id)
}
function userProfileLink(username: string) {
return !username || username.includes('@')
? undefined
: () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
return !username || username.includes('@') ? undefined : `/user/${encodeURIComponent(username)}`
}
async function requestAuth(flow: ModrinthAuthFlow) {
await auth.requestSignIn(`/instance/${encodeURIComponent(props.instance.id)}/share`, flow, {
@@ -157,7 +157,6 @@ import {
useRelativeTime,
useVIntl,
} from '@modrinth/ui'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, ref } from 'vue'
import {
@@ -294,9 +293,7 @@ const messages = defineMessages({
},
})
function userProfileLink(username: string) {
return !username || username.includes('@')
? undefined
: () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
return !username || username.includes('@') ? undefined : `/user/${encodeURIComponent(username)}`
}
function setUsernameRef(id: string, element: Element | null) {
usernameRefs.value[id] = element instanceof HTMLElement ? element : null
@@ -1,17 +0,0 @@
<script setup lang="ts">
import GridDisplay from '@/components/GridDisplay.vue'
defineProps({
instances: {
type: Array,
required: true,
},
})
</script>
<template>
<GridDisplay
v-if="instances && instances.length > 0"
label="Instances"
:instances="instances.filter((i) => !i.link)"
/>
</template>
@@ -1,17 +0,0 @@
<script setup lang="ts">
import GridDisplay from '@/components/GridDisplay.vue'
defineProps({
instances: {
type: Array,
required: true,
},
})
</script>
<template>
<GridDisplay
v-if="instances && instances.length > 0"
label="Instances"
:instances="instances.filter((i) => i.link)"
/>
</template>
@@ -1,89 +0,0 @@
<script setup lang="ts">
import { PlusIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, NavTabs } from '@modrinth/ui'
import { inject, onUnmounted, ref, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
import { NewInstanceImage } from '@/assets/icons'
import { instance_listener } from '@/helpers/events.js'
import { list } from '@/helpers/instance'
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
const { handleError } = injectNotificationManager()
const showCreationModal = inject('showCreationModal')
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
breadcrumbs.setRootContext({ name: 'Library', link: route.path })
const instances = shallowRef(await list().catch(handleError))
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
offline.value = true
})
window.addEventListener('online', () => {
offline.value = false
})
const unlistenInstance = await instance_listener(async () => {
instances.value = await list().catch(handleError)
})
onUnmounted(() => {
unlistenInstance()
})
</script>
<template>
<div class="p-6 flex flex-col gap-3">
<h1 class="m-0 text-2xl hidden">Library</h1>
<NavTabs
:links="[
{ label: 'All instances', href: `/library` },
{ label: 'Modpacks', href: `/library/modpacks` },
{ label: 'Servers', href: `/library/servers` },
{ label: 'Custom', href: `/library/custom` },
{ label: 'Shared with me', href: `/library/shared`, shown: false },
{ label: 'Saved', href: `/library/saved`, shown: false },
]"
/>
<template v-if="instances && instances.length > 0">
<RouterView v-if="route.path.startsWith('/library')" :instances="instances" />
</template>
<div v-else class="no-instance">
<div class="icon">
<NewInstanceImage />
</div>
<h3>No instances found</h3>
<ButtonStyled color="brand">
<button :disabled="offline" @click="showCreationModal?.()">
<PlusIcon />
Create new instance
</button>
</ButtonStyled>
</div>
</div>
</template>
<style lang="scss" scoped>
.no-instance {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--gap-md);
p,
h3 {
margin: 0;
}
.icon {
svg {
width: 10rem;
height: 10rem;
}
}
}
</style>
@@ -1,47 +0,0 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import GridDisplay from '@/components/GridDisplay.vue'
import { get_project_v3_many } from '@/helpers/cache.js'
const props = defineProps({
instances: {
type: Array,
required: true,
},
})
const serverProjectIds = ref(new Set())
const linkedInstances = computed(() => props.instances.filter((i) => i.link))
watchEffect(async () => {
const projectIds = [
...new Set(linkedInstances.value.map((i) => i.link?.project_id).filter(Boolean)),
]
if (projectIds.length === 0) {
serverProjectIds.value = new Set()
return
}
try {
const projects = await get_project_v3_many(projectIds, 'must_revalidate')
serverProjectIds.value = new Set(
projects.filter((p) => p?.minecraft_server != null).map((p) => p.id),
)
} catch {
serverProjectIds.value = new Set()
}
})
const filteredInstances = computed(() =>
linkedInstances.value.filter((i) => !serverProjectIds.value.has(i.link?.project_id)),
)
</script>
<template>
<GridDisplay
v-if="filteredInstances && filteredInstances.length > 0"
label="Instances"
:instances="filteredInstances"
/>
</template>
@@ -1,13 +0,0 @@
<script setup lang="ts">
import GridDisplay from '@/components/GridDisplay.vue'
defineProps({
instances: {
type: Array,
required: true,
},
})
</script>
<template>
<GridDisplay v-if="instances && instances.length > 0" label="Instances" :instances="instances" />
</template>
@@ -1,47 +0,0 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import GridDisplay from '@/components/GridDisplay.vue'
import { get_project_v3_many } from '@/helpers/cache.js'
const props = defineProps({
instances: {
type: Array,
required: true,
},
})
const serverProjectIds = ref(new Set())
const linkedInstances = computed(() => props.instances.filter((i) => i.link))
watchEffect(async () => {
const projectIds = [
...new Set(linkedInstances.value.map((i) => i.link?.project_id).filter(Boolean)),
]
if (projectIds.length === 0) {
serverProjectIds.value = new Set()
return
}
try {
const projects = await get_project_v3_many(projectIds, 'must_revalidate')
serverProjectIds.value = new Set(
projects.filter((p) => p?.minecraft_server != null).map((p) => p.id),
)
} catch {
serverProjectIds.value = new Set()
}
})
const filteredInstances = computed(() =>
linkedInstances.value.filter((i) => serverProjectIds.value.has(i.link?.project_id)),
)
</script>
<template>
<GridDisplay
v-if="filteredInstances && filteredInstances.length > 0"
label="Instances"
:instances="filteredInstances"
/>
</template>
@@ -1,8 +0,0 @@
import Custom from './Custom.vue'
import Downloaded from './Downloaded.vue'
import Index from './Index.vue'
import Modpacks from './Modpacks.vue'
import Overview from './Overview.vue'
import Servers from './Servers.vue'
export { Custom, Downloaded, Index, Modpacks, Overview, Servers }
@@ -31,8 +31,9 @@
:organization="organization"
:members="members"
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
:user-link="(username) => `https://modrinth.com/user/${username}`"
:user-link="(username) => `/user/${encodeURIComponent(username)}`"
link-target="_blank"
:user-link-target="null"
class="project-sidebar-section"
/>
<ProjectSidebarDetails
@@ -147,6 +147,7 @@ export interface ContentInstallContext {
loader: string
gameVersion: string
}) => Promise<void>
prepareNewInstance: (projectId: string) => Promise<void>
handleNavigate: (instance: ContentInstallInstance) => void
handleCancel: () => void
setContentInstallModal: (ref: ModalRef) => void
@@ -425,7 +426,12 @@ export function createContentInstall(opts: {
project: Labrinth.Projects.v2.Project,
versions: Labrinth.Versions.v2.Version[],
onInstall: ContentInstallCallback,
hints?: { preferredLoader?: string; preferredGameVersion?: string; showProjectInfo?: boolean },
hints?: {
preferredLoader?: string
preferredGameVersion?: string
showProjectInfo?: boolean
showModal?: boolean
},
) {
currentProject = project
currentVersions = versions
@@ -435,6 +441,7 @@ export function createContentInstall(opts: {
loading.value = true
defaultTab.value = 'existing'
let projectInfoPromise: Promise<unknown> = Promise.resolve()
if (hints?.showProjectInfo) {
projectInfo.value = {
title: project.title,
@@ -442,7 +449,7 @@ export function createContentInstall(opts: {
link: `/project/${project.slug ?? project.id}`,
}
if (project.organization) {
get_organization(project.organization)
projectInfoPromise = get_organization(project.organization)
.then((org: { id: string; slug: string; name: string; icon_url?: string }) => {
if (projectInfo.value) {
const orgSlug = org.slug ?? org.id
@@ -459,7 +466,7 @@ export function createContentInstall(opts: {
})
.catch(() => {})
} else if (project.team) {
get_team(project.team)
projectInfoPromise = get_team(project.team)
.then(
(
members: {
@@ -475,7 +482,7 @@ export function createContentInstall(opts: {
name: owner.user.username,
iconUrl: owner.user.avatar_url,
circle: true,
link: () => openUrl(`https://modrinth.com/user/${owner.user.username}`),
link: `/user/${encodeURIComponent(owner.user.username)}`,
},
}
}
@@ -510,10 +517,12 @@ export function createContentInstall(opts: {
: null
await nextTick()
modalRef?.show()
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
if (hints?.showModal !== false) {
modalRef?.show()
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
}
get_game_versions()
const gameVersionMetadataPromise = get_game_versions()
.then((allGameVersions) => {
const releases = new Set<string>()
const ordered: string[] = []
@@ -560,6 +569,24 @@ export function createContentInstall(opts: {
} finally {
loading.value = false
}
await gameVersionMetadataPromise
await projectInfoPromise
}
async function prepareNewInstance(projectId: string) {
const project: Labrinth.Projects.v2.Project = await get_project(projectId, 'must_revalidate')
if (!project || project.project_type === 'modpack') {
throw new Error(`Project cannot be prepared as a new instance: '${projectId}'`)
}
const versions = (
(await get_version_many(project.versions)) as Labrinth.Versions.v2.Version[]
).sort((a, b) => dayjs(b.date_published).valueOf() - dayjs(a.date_published).valueOf())
await showModInstallModal(project, versions, () => {}, {
showProjectInfo: true,
showModal: false,
})
}
function getInstallTargets(versions: Labrinth.Versions.v2.Version[]) {
@@ -926,6 +953,7 @@ export function createContentInstall(opts: {
projectInfo,
handleInstallToInstance,
handleCreateAndInstall,
prepareNewInstance,
handleNavigate,
handleCancel,
setContentInstallModal(ref: ModalRef) {
@@ -0,0 +1,60 @@
import { createContext } from '@modrinth/ui'
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import { computed, type ComputedRef, onUnmounted, ref } from 'vue'
import { getOnboardingChecklist, type OnboardingChecklist } from '@/helpers/onboarding-checklist'
export interface OnboardingChecklistContext {
hasCreatedInstance: ComputedRef<boolean>
hasLoggedIntoMinecraft: ComputedRef<boolean>
hasLoggedIntoModrinth: ComputedRef<boolean>
isReady: ComputedRef<boolean>
showChecklist: ComputedRef<boolean>
}
export interface OnboardingChecklistProvider extends OnboardingChecklistContext {
initialize: () => Promise<void>
}
export const [injectOnboardingChecklist, provideOnboardingChecklist] =
createContext<OnboardingChecklistContext>('root', 'onboardingChecklist')
export function setupOnboardingChecklistProvider(): OnboardingChecklistProvider {
const checklist = ref<OnboardingChecklist>()
let unlisten: UnlistenFn | undefined
const context: OnboardingChecklistContext = {
hasCreatedInstance: computed(() => checklist.value?.has_created_instance ?? false),
hasLoggedIntoMinecraft: computed(() => checklist.value?.has_logged_into_minecraft ?? false),
hasLoggedIntoModrinth: computed(() => checklist.value?.has_logged_into_modrinth ?? false),
isReady: computed(() => checklist.value !== undefined),
showChecklist: computed(() => checklist.value?.show_checklist ?? false),
}
provideOnboardingChecklist(context)
const applyChecklist = (nextChecklist: OnboardingChecklist) => {
checklist.value = {
has_created_instance:
(checklist.value?.has_created_instance ?? false) || nextChecklist.has_created_instance,
has_logged_into_minecraft:
(checklist.value?.has_logged_into_minecraft ?? false) ||
nextChecklist.has_logged_into_minecraft,
has_logged_into_modrinth:
(checklist.value?.has_logged_into_modrinth ?? false) ||
nextChecklist.has_logged_into_modrinth,
show_checklist: (checklist.value?.show_checklist ?? true) && nextChecklist.show_checklist,
}
}
const initialize = async () => {
unlisten = await listen<OnboardingChecklist>('onboarding_checklist', (event) => {
applyChecklist(event.payload)
})
applyChecklist(await getOnboardingChecklist())
}
onUnmounted(() => unlisten?.())
return { ...context, initialize }
}
+3
View File
@@ -1,5 +1,6 @@
import type { AbstractPopupNotificationManager, AbstractWebNotificationManager } from '@modrinth/ui'
import { setupOnboardingChecklistProvider } from './onboarding-checklist'
import { setupCreationModal } from './setup/creation-modal'
import { setupFileDropProvider } from './setup/file-drop'
import { setupFilePickerProvider } from './setup/file-picker'
@@ -14,8 +15,10 @@ export function setupProviders(
setupFileDropProvider()
setupFilePickerProvider()
setupInstanceImportProvider(notificationManager)
const onboardingChecklist = setupOnboardingChecklistProvider()
return {
...setupCreationModal(notificationManager),
onboardingChecklist,
}
}
@@ -10,7 +10,7 @@ import { useRouter } from 'vue-router'
import type UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_search_results } from '@/helpers/cache.js'
import { get_search_results } from '@/helpers/cache.js'
import { import_instance } from '@/helpers/import.js'
import {
type CreatePackLocation,
@@ -48,6 +48,10 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
provide('showCreationModal', () => {
installationModal.value?.show()
})
provide('showImportModal', async () => {
await installationModal.value?.show()
installationModal.value?.ctx.setImportMode()
})
async function proceedWithModpackCreation(
projectId: string,
@@ -181,8 +185,10 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
router.push('/browse/modpack')
}
async function searchModpacks(query: string, limit: number = 10) {
const params = [`facets=[["project_type:modpack"]]`, `limit=${limit}`]
async function searchProjects(query: string, limit: number = 10) {
const projectTypes = ['mod', 'modpack', 'resourcepack', 'shader', 'datapack']
const facets = JSON.stringify([projectTypes.map((type) => `project_type:${type}`)])
const params = [`facets=${encodeURIComponent(facets)}`, `limit=${limit}`]
if (query) {
params.push(`query=${encodeURIComponent(query)}`)
}
@@ -191,19 +197,13 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
return { hits: [], offset: 0, limit, total_hits: 0 }
}
async function getProjectVersions(projectId: string) {
const versions = await get_project_versions(projectId)
return versions ?? []
}
return {
installationModal,
unknownPackWarningModal,
fetchExistingInstanceNames,
handleCreate,
handleBrowseModpacks,
searchModpacks,
getProjectVersions,
searchProjects,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
@@ -9,9 +9,9 @@ import {
getStoredServerAddonInstallQueue,
injectModrinthClient,
injectNotificationManager,
type ModpackSearchResult,
type PendingServerContentInstall,
type PendingServerContentInstallType,
type ProjectSearchResult,
readPendingServerContentInstalls,
readStoredServerInstallQueue,
removePendingServerContentInstall,
@@ -69,7 +69,7 @@ export interface ServerInstallContentContext {
installQueuedServerInstallsAndBack: () => Promise<boolean>
initServerContext: () => Promise<void>
watchServerContextChanges: () => void
searchServerModpacks: (query: string, limit?: number) => Promise<ModpackSearchResult>
searchServerModpacks: (query: string, limit?: number) => Promise<ProjectSearchResult>
getServerProjectVersions: (projectId: string) => Promise<{ id: string }[]>
enforceSetupModpackRoute: (currentProjectType: string | undefined) => void
getQueuedServerInstallPlans: () => Map<string, BrowseInstallPlan<InstallableSearchResult>>
@@ -108,7 +108,7 @@ function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
id: ownerId,
name: project.author,
type: 'user' as const,
link: `https://modrinth.com/user/${ownerId}`,
link: `/user/${encodeURIComponent(ownerId)}`,
}
}
@@ -144,7 +144,7 @@ async function getQueuedInstallOwner(
name: owner.username,
type: 'user' as const,
avatar_url: owner.avatar_url,
link: `https://modrinth.com/user/${owner.username}`,
link: `/user/${encodeURIComponent(owner.username)}`,
}
}
} catch {
+4 -32
View File
@@ -3,7 +3,6 @@ import { createRouter, createWebHistory } from 'vue-router'
import * as Pages from '@/pages'
import * as Hosting from '@/pages/hosting/manage'
import * as Instance from '@/pages/instance'
import * as Library from '@/pages/library'
import * as Project from '@/pages/project'
/**
@@ -101,39 +100,12 @@ export default new createRouter({
},
},
{
path: '/library',
name: 'Library',
component: Library.Index,
path: '/user/:user/:projectType?',
name: 'User',
component: Pages.User,
meta: {
breadcrumb: [{ name: 'Library' }],
breadcrumb: [{ name: '?User' }],
},
children: [
{
path: '',
name: 'Overview',
component: Library.Overview,
},
{
path: 'downloaded',
name: 'Downloaded',
component: Library.Downloaded,
},
{
path: 'modpacks',
name: 'Modpacks',
component: Library.Modpacks,
},
{
path: 'servers',
name: 'LibraryServers',
component: Library.Servers,
},
{
path: 'custom',
name: 'Custom',
component: Library.Custom,
},
],
},
{
path: '/:projectType(mod|plugin|datapack|resourcepack|shader|modpack)/:id/:rest(.*)*',
+1
View File
@@ -17,6 +17,7 @@ export const DEFAULT_FEATURE_FLAGS = {
i18n_debug: false,
show_instance_play_time: true,
advanced_filters_collapsed: true,
always_show_copy_details: false,
}
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
+2 -2
View File
@@ -55,11 +55,11 @@ core-foundation.workspace = true
core-graphics.workspace = true
objc2-app-kit = { workspace = true, features = ["NSWindow"] }
[target.'cfg(windows)'.dependencies]
[target."cfg(windows)".dependencies]
webview2-com.workspace = true
windows-core.workspace = true
[target.'cfg(windows)'.dependencies.windows]
[target."cfg(windows)".dependencies.windows]
workspace = true
features = [
"Win32_Foundation",
+20 -1
View File
@@ -140,6 +140,14 @@ fn main() {
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"onboarding-checklist",
InlinedPlugin::new()
.commands(&["get_onboarding_checklist"])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
)
.plugin(
"install",
InlinedPlugin::new()
@@ -207,6 +215,10 @@ fn main() {
"instance_get_full_path",
"instance_get_mod_full_path",
"instance_list",
"instance_list_groups",
"instance_create_group",
"instance_rename_group",
"instance_delete_group",
"instance_check_installed",
"instance_update_all",
"instance_update_project",
@@ -276,7 +288,14 @@ fn main() {
.plugin(
"users",
InlinedPlugin::new()
.commands(&["search_user"])
.commands(&[
"search_user",
"get_user_profile",
"get_user_projects",
"get_user_organizations",
"get_user_collections",
"patch_user",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
),
+1
View File
@@ -99,6 +99,7 @@
"metadata:default",
"minecraft-skins:default",
"mr-auth:default",
"onboarding-checklist:default",
"instance:default",
"install:default",
"process:default",

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