Compare commits

...
Author SHA1 Message Date
tdgao 188ab15f1a feat: welcome screen styles match figma 2026-07-30 16:22:02 -07:00
tdgao 6e06623e0f feat: add group by instance type and change group sorting 2026-07-30 15:51:55 -07:00
tdgao 8f8e45a29d feat: improve the "New group" jank 2026-07-30 15:46:12 -07:00
tdgao c063ca1ed5 remove manual sort 2026-07-30 15:37:09 -07:00
tdgao 25ca13b74c feat: add/remove favourites group 2026-07-30 15:32:24 -07:00
tdgao 88b694976b feat: add group action buttons 2026-07-30 14:49:40 -07:00
tdgao 98a17ae307 feat: better context menu options 2026-07-30 14:33:28 -07:00
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
329 changed files with 19502 additions and 8666 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

@@ -0,0 +1,39 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20.3429" height="7.68571" viewBox="0 0 20.3429 7.68571" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 50789">
<g id="Ellipse 1140" filter="url(#filter0_dii_0_10)">
<circle cx="3.84286" cy="3.4619" r="2.5" fill="url(#paint0_linear_0_10)" fill-opacity="0.9" shape-rendering="crispEdges"/>
<circle cx="3.84286" cy="3.4619" r="2.6" stroke="black" stroke-opacity="0.1" stroke-width="0.2" shape-rendering="crispEdges"/>
</g>
<circle id="Ellipse 1141" cx="10.8429" cy="3.4619" r="2.5" fill="black" fill-opacity="0.37"/>
<circle id="Ellipse 1142" cx="17.8429" cy="3.4619" r="2.5" fill="black" fill-opacity="0.37"/>
</g>
<defs>
<filter id="filter0_dii_0_10" x="4.47035e-08" y="1.49012e-08" width="7.68571" height="7.68571" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.380952"/>
<feGaussianBlur stdDeviation="0.571429"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.32 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_10"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_10" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.4"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_0_10"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="-0.4"/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="effect2_innerShadow_0_10" result="effect3_innerShadow_0_10"/>
</filter>
<linearGradient id="paint0_linear_0_10" x1="3.84286" y1="0.961905" x2="3.84286" y2="5.9619" gradientUnits="userSpaceOnUse">
<stop stop-color="#1BD9A9"/>
<stop offset="0.45" stop-color="#1BD96A"/>
<stop offset="1" stop-color="#1BD951"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,32 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="74.0007" height="74" viewBox="0 0 74.0007 74" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 49552">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M72.7343 46.6308C74.0593 41.6962 74.3484 36.5409 73.5835 31.4891C72.8185 26.4374 71.016 21.5985 68.2894 17.2769C65.5627 12.9553 61.9709 9.2445 57.7399 6.37785C53.5089 3.5112 48.7302 1.55077 43.7044 0.619851C38.6786 -0.311064 33.5144 -0.192312 28.5368 0.968637C23.5591 2.12959 18.8757 4.30761 14.781 7.36571C10.6863 10.4238 7.26903 14.2958 4.74408 18.7382C2.21913 23.1805 0.641152 28.0971 0.109401 33.1787H6.37602C7.23467 26.3912 10.338 20.0852 15.193 15.2629C20.048 10.4405 26.376 7.37843 33.1716 6.56315C39.9671 5.74787 46.8403 7.22619 52.699 10.7632C58.5577 14.3002 63.0657 19.693 65.5068 26.0846L59.4177 27.7133C58.0562 24.4413 55.9951 21.5067 53.379 19.1153C50.7629 16.7238 47.655 14.9334 44.2734 13.8697L43.1506 20.2016C46.469 21.4193 49.3529 23.5909 51.4394 26.4432C53.5258 29.2955 54.7216 32.701 54.8763 36.2311C55.0309 39.7612 54.1375 43.2581 52.3084 46.2818C50.4793 49.3055 47.7964 51.7208 44.5972 53.2237L46.263 59.4356C51.1999 57.392 55.325 53.7784 57.9996 49.1543C60.6741 44.5302 61.7489 39.1537 61.0575 33.8573L67.1257 32.2391C67.7664 36.2855 67.5781 40.4199 66.5721 44.3914L72.7343 46.6308Z" fill="url(#paint0_linear_0_14)"/>
<path d="M28.5938 1.21191C33.5378 0.058849 38.6674 -0.0593858 43.6592 0.865234C48.651 1.78989 53.3972 3.73771 57.5996 6.58496C61.8021 9.43226 65.3699 13.1178 68.0782 17.4102C70.7863 21.7025 72.5762 26.5088 73.336 31.5264C74.0821 36.4539 73.8157 41.4799 72.5596 46.3008L66.8672 44.2324C67.8334 40.2956 68.0063 36.2054 67.3721 32.2002L67.3291 31.9258L67.0615 31.9971L60.9932 33.6162L60.7813 33.6719L60.8096 33.8896C61.4939 39.1315 60.4302 44.4528 57.7832 49.0293C55.1838 53.5234 51.1993 57.0519 46.4297 59.0928L44.8916 53.3564C48.0518 51.827 50.7041 49.4171 52.5225 46.4111C54.377 43.3453 55.2828 39.7991 55.126 36.2197C54.9691 32.6407 53.757 29.1878 51.6416 26.2959C49.5672 23.46 46.714 21.2894 43.4317 20.042L44.4688 14.1973C47.7105 15.2594 50.6909 16.9971 53.21 19.2998C55.7992 21.6667 57.8391 24.5713 59.1865 27.8096L59.2705 28.0117L59.4824 27.9551L65.5713 26.3262L65.8389 26.2549L65.7403 25.9951C63.2793 19.5515 58.7345 14.1146 52.8282 10.5488C46.9218 6.9831 39.9924 5.49255 33.1416 6.31445C26.2908 7.13639 19.9111 10.2243 15.0166 15.0859C10.1745 19.8957 7.06234 26.1694 6.15823 32.9287H0.390651C0.946388 27.9796 2.49885 23.1931 4.96096 18.8613C7.46884 14.449 10.8637 10.6038 14.9307 7.56641C18.9977 4.52894 23.6497 2.36503 28.5938 1.21191Z" stroke="url(#paint1_linear_0_14)" stroke-opacity="0.4" stroke-width="0.5"/>
</g>
<g id="Vector_2">
<path d="M46.5453 72.7337C41.2782 74.1472 35.7639 74.3806 30.3962 73.4174C25.0284 72.4542 19.9396 70.318 15.4933 67.1615C11.047 64.005 7.35277 59.9061 4.67454 55.1574C1.99632 50.4086 0.400136 45.1274 0 39.6907H6.26662C6.42964 41.4382 6.7406 43.1688 7.19617 44.8638C7.67146 46.6415 8.3059 48.3729 9.09182 50.0369L14.6691 46.6908C13.0374 42.9123 12.3917 38.7817 12.7928 34.6857C13.1938 30.5898 14.6283 26.6626 16.9619 23.272C19.2956 19.8815 22.452 17.1385 26.1355 15.3C29.8191 13.4615 33.9092 12.5877 38.0227 12.7604L36.8895 19.0923C34.1456 19.1048 31.4411 19.7458 28.9837 20.966C26.5263 22.1863 24.3815 23.9533 22.7138 26.1314C21.0462 28.3095 19.9001 30.8407 19.3638 33.5306C18.8274 36.2205 18.915 38.9974 19.6197 41.6482C19.7816 42.2381 19.954 42.8227 20.1785 43.3447L27.312 39.0695L25.1761 33.3901L31.9128 26.4735L40.4301 24.6412L42.8793 27.6845L38.9523 31.657L35.5317 32.7323L33.0825 35.2484L34.2836 38.5788C34.2836 38.5788 36.7067 41.1575 36.7119 41.1575L40.1429 40.2492L42.5817 37.5713L47.9083 35.88L49.475 39.4506L44.0021 46.1792L34.7902 49.0868L30.6542 44.4879L23.4528 48.8154C25.3066 50.9244 27.6287 52.5701 30.2332 53.6207C32.8376 54.6713 35.652 55.0976 38.4509 54.8654L40.1168 61.093C35.96 61.6355 31.7334 61.0954 27.8468 59.5251C23.9601 57.9548 20.5452 55.4075 17.933 52.1301L12.3766 55.4605C15.55 59.6249 19.736 62.909 24.5369 65.0007C29.3377 67.0923 34.5939 67.922 39.806 67.411C45.018 66.8999 50.0128 65.065 54.3155 62.0808C58.6183 59.0965 62.0861 55.0619 64.3895 50.3605L70.5517 52.5999C68.2702 57.509 64.9374 61.8569 60.789 65.3361C56.6406 68.8153 51.778 71.3407 46.5453 72.7337Z" fill="url(#paint2_linear_0_14)"/>
<path d="M37.7246 13.0026L36.6797 18.8464C33.9693 18.8898 31.3013 19.5356 28.8721 20.7419C26.3807 21.979 24.2064 23.771 22.5156 25.9792C20.8248 28.1876 19.662 30.7547 19.1182 33.4821C18.5744 36.2093 18.6634 39.025 19.3779 41.7126L19.3789 41.7145C19.541 42.3051 19.7173 42.9048 19.9492 43.444L20.0615 43.7057L20.3066 43.5593L27.4404 39.2839L27.6191 39.1764L27.5459 38.9811L25.4658 33.4499L32.0391 26.7009L40.332 24.9177L42.5439 27.6667L38.8174 31.4372L35.457 32.4938L35.3965 32.5124L35.3525 32.5583L32.9033 35.0739L32.7939 35.1862L32.8477 35.3337L34.0488 38.6637L34.0664 38.7126L34.1016 38.7497V38.7507L34.1035 38.7516C34.1047 38.7529 34.1061 38.7551 34.1084 38.7575C34.113 38.7624 34.1201 38.7696 34.1289 38.779C34.1467 38.7979 34.173 38.8258 34.2061 38.861C34.2723 38.9315 34.367 39.0323 34.4805 39.153C34.7077 39.3948 35.0103 39.7174 35.3135 40.0397C35.6167 40.3621 35.9204 40.6846 36.1484 40.9264C36.2623 41.0473 36.3579 41.1478 36.4248 41.2184C36.4582 41.2537 36.4845 41.2822 36.5029 41.3014C36.512 41.3109 36.5199 41.3183 36.5254 41.3239C36.5279 41.3265 36.5305 41.33 36.5332 41.3327C36.5343 41.3338 36.5369 41.3366 36.54 41.3395C36.5412 41.3406 36.5462 41.3446 36.5518 41.3493C36.5552 41.3521 36.5642 41.3596 36.5703 41.3639C36.5845 41.3729 36.6539 41.4011 36.7119 41.4079H36.7441L36.7764 41.3991L40.207 40.4909L40.2783 40.4723L40.3281 40.4177L42.7217 37.7878L47.7695 36.1852L49.1846 39.4108L43.8545 45.9636L34.8672 48.7995L30.8398 44.321L30.7021 44.1677L30.5254 44.2741L23.3242 48.6012L23.0674 48.7555L23.2646 48.9802C25.1442 51.1184 27.499 52.787 30.1396 53.8522C32.7153 54.8912 35.4932 55.3261 38.2617 55.1286L39.7998 60.8776C35.7775 61.354 31.6985 60.812 27.9404 59.2936C24.0938 57.7395 20.7143 55.2179 18.1289 51.9743L17.9922 51.8034L17.8047 51.9157L12.248 55.2458L12.0088 55.3893L12.1777 55.612C15.3769 59.8102 19.5967 63.1215 24.4365 65.2302C29.2764 67.3388 34.5757 68.175 39.8301 67.6598C45.0845 67.1446 50.1202 65.2944 54.458 62.2858C58.734 59.3201 62.1908 55.3249 64.5127 50.6716L70.2061 52.7399C67.9437 57.5147 64.6786 61.747 60.6279 65.1442C56.5078 68.5997 51.6786 71.1083 46.4814 72.4919H46.4805C41.249 73.8958 35.7719 74.1283 30.4404 73.1716C25.1089 72.2148 20.054 70.0929 15.6377 66.9577C11.2216 63.8226 7.55266 59.7513 4.89258 55.0348C2.27662 50.3965 0.702069 45.2462 0.272461 39.9411H6.04102C6.18839 41.4053 6.43742 42.8572 6.78906 44.2858L6.95508 44.9284C7.43423 46.7205 8.07394 48.4658 8.86621 50.1432L8.9834 50.3932L9.2207 50.2516L14.7979 46.905L14.9854 46.7927L14.8984 46.5915C13.2835 42.8519 12.6451 38.7634 13.042 34.7096C13.4389 30.656 14.8584 26.7693 17.168 23.4137C19.4776 20.0582 22.6015 17.3437 26.2471 15.5241C29.8069 13.7473 33.7504 12.8833 37.7246 13.0026Z" stroke="url(#paint3_linear_0_14)" stroke-opacity="0.4" stroke-width="0.5"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_0_14" x1="27.7452" y1="-15.7583" x2="40.9431" y2="62.6745" gradientUnits="userSpaceOnUse">
<stop stop-color="#1BD9AA"/>
<stop offset="0.450385" stop-color="#1BD96A"/>
<stop offset="1" stop-color="#1BD951"/>
</linearGradient>
<linearGradient id="paint1_linear_0_14" x1="37.055" y1="3.26749e-09" x2="37.055" y2="59.4356" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.4"/>
</linearGradient>
<linearGradient id="paint2_linear_0_14" x1="26.3868" y1="-3.50338" x2="41.0049" y2="76.972" gradientUnits="userSpaceOnUse">
<stop stop-color="#1BD9AA"/>
<stop offset="0.450385" stop-color="#1BD96A"/>
<stop offset="1" stop-color="#1BD951"/>
</linearGradient>
<linearGradient id="paint3_linear_0_14" x1="35.2758" y1="12.7389" x2="35.2758" y2="74" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.4"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 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,154 @@
<script setup lang="ts">
import { ImportIcon, PlusIcon } from '@modrinth/assets'
import { ButtonStyled } from '@modrinth/ui'
import { inject, onMounted, onUnmounted, ref } from 'vue'
import frog from '../../assets/welcome/frog.png'
import iconLights from '../../assets/welcome/icon-lights.svg?url'
import iconLogo from '../../assets/welcome/icon-logo.svg?url'
import iconTexture from '../../assets/welcome/icon-texture.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-[30.8125rem_1fr_auto] px-6 pb-6 pt-16">
<div class="relative flex items-end justify-center">
<div
class="dot-pattern pointer-events-none absolute left-1/2 top-0 h-[29.875rem] w-[min(25.9375rem,80vw)] -translate-x-1/2 rounded-2xl [@media(max-height:700px)]:h-[23rem]"
aria-hidden="true"
/>
<div class="relative flex flex-col items-center gap-6">
<div
class="welcome-icon relative h-[6.25rem] w-[6.25rem] overflow-hidden rounded-3xl border-[1.5px] border-solid bg-surface-1"
style="border-color: color-mix(in srgb, var(--color-text-primary) 15%, transparent)"
>
<div
class="absolute inset-0 bg-[linear-gradient(180deg,rgb(37_114_79)_0%,rgb(15_64_36)_100%)] opacity-50"
/>
<div
class="absolute inset-0 bg-[linear-gradient(-14deg,rgb(0_0_0_/_37%)_8%,transparent_86%)]"
/>
<img
:src="iconTexture"
alt=""
class="absolute left-1/2 top-1/2 h-[12.70625rem] w-[19.96875rem] max-w-none -translate-x-1/2 -translate-y-1/2 object-cover opacity-40 mix-blend-luminosity"
/>
<img :src="iconLogo" alt="" class="absolute inset-[13%] h-[74%] w-[74%]" />
<img
:src="iconLights"
alt=""
class="absolute left-[0.4775rem] top-[0.4775rem] h-[0.4804rem] w-[1.2714rem]"
/>
</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
<img :src="frog" alt="" class="h-8 w-8 [image-rendering:pixelated]" />
</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-end gap-4 text-sm leading-5 text-secondary">
<span class="whitespace-nowrap">Escaping another launcher?</span>
<ButtonStyled>
<button
class="!h-10 !gap-2 !rounded-[0.875rem] !px-4 !py-2.5 !font-medium"
:disabled="offline"
@click="showImportModal?.()"
>
<ImportIcon />
Import from launcher
</button>
</ButtonStyled>
</div>
</div>
</template>
<style scoped>
.dot-pattern {
background-image: radial-gradient(
circle,
color-mix(in srgb, var(--color-text-primary) 25%, transparent) 0.5px,
transparent 0.75px
);
background-size: 0.5625rem 0.5625rem;
opacity: 0.8;
-webkit-mask-image: radial-gradient(ellipse at center, black 10%, transparent 68%);
mask-image: radial-gradient(ellipse at center, black 10%, transparent 68%);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
}
.welcome-icon {
box-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 color-mix(in srgb, var(--color-brand) 32%, transparent);
}
</style>
@@ -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,198 @@
<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 v-if="displayState.group === 'Group'" type="transparent">
<button type="button" :disabled="busy" @click="createGroupFromSelection">
<SquarePlusIcon />
<span class="bar-label">{{ formatMessage(messages.newGroup) }}</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, SquarePlusIcon, 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,
displayState,
} = 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',
},
newGroup: {
id: 'app.library.selection.new-group',
defaultMessage: 'New group',
},
removeFromGroup: {
id: 'app.library.selection.remove-from-group',
defaultMessage: 'Remove from group',
},
})
async function createGroupFromSelection() {
if (busy.value) return
const instanceIds = selectedInstanceIds.value
clearLibraryInstanceSelection()
await createDefaultGroup(instanceIds)
}
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,153 @@
<template>
<NewModal
ref="modal"
no-padding
scrollable
actions-divider
max-width="560px"
width="560px"
:on-hide="closeGroupInstancesModal"
>
<template #title>
<span class="text-2xl font-semibold text-contrast">
{{
formatMessage(messages.title, {
groupName: groupInstancesModalGroup?.name ?? '',
})
}}
</span>
</template>
<div class="flex h-[400px] flex-col gap-3 overflow-y-auto bg-surface-2 py-4">
<div class="px-6">
<StyledInput
v-model="groupInstancesSearch"
:icon="SearchIcon"
:placeholder="formatMessage(messages.searchPlaceholder)"
class="w-full"
/>
</div>
<div
v-if="groupInstances.length === 0"
class="flex items-center justify-center py-12 text-secondary"
>
{{ formatMessage(messages.noInstancesFound) }}
</div>
<div v-else class="flex flex-col gap-1">
<div
v-for="instance in groupInstances"
:key="instance.id"
class="flex items-center justify-between gap-4 px-6 py-1.5 hover:bg-surface-3"
:class="{ 'opacity-60': selectedGroupInstanceIds.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"
/>
<span class="truncate font-semibold text-contrast">{{ instance.name }}</span>
</div>
<ButtonStyled :type="selectedGroupInstanceIds.has(instance.id) ? 'outlined' : 'standard'">
<button type="button" @click="toggleGroupInstance(instance.id)">
<CheckIcon v-if="selectedGroupInstanceIds.has(instance.id)" />
{{
formatMessage(
selectedGroupInstanceIds.has(instance.id) ? messages.added : messages.add,
)
}}
</button>
</ButtonStyled>
</div>
</div>
</div>
<template #actions>
<div class="flex items-center justify-end gap-2">
<ButtonStyled type="outlined">
<button type="button" @click="modal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="brand">
<button type="button" :disabled="savingGroupInstances" @click="save">
<SpinnerIcon v-if="savingGroupInstances" class="animate-spin" />
<CheckIcon v-else />
{{ formatMessage(commonMessages.saveChangesButton) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
</template>
<script setup lang="ts">
import { CheckIcon, SearchIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
commonMessages,
defineMessages,
NewModal,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core'
import { ref, watch } from 'vue'
import { useLibrary } from '@/components/ui/library/use-library'
const { formatMessage } = useVIntl()
const messages = defineMessages({
title: {
id: 'app.library.group.instances-modal.title',
defaultMessage: 'Add instances to "{groupName}"',
},
searchPlaceholder: {
id: 'app.library.group.instances-modal.search-placeholder',
defaultMessage: 'Search instance',
},
noInstancesFound: {
id: 'app.library.group.instances-modal.no-instances-found',
defaultMessage: 'No instances found',
},
add: {
id: 'app.library.group.instances-modal.add',
defaultMessage: 'Add',
},
added: {
id: 'app.library.group.instances-modal.added',
defaultMessage: 'Added',
},
})
const {
isGroupInstancesModalOpen,
groupInstancesModalGroup,
groupInstancesSearch,
groupInstances,
selectedGroupInstanceIds,
savingGroupInstances,
closeGroupInstancesModal,
toggleGroupInstance,
saveGroupInstances,
} = useLibrary()
const modal = ref<InstanceType<typeof NewModal>>()
watch(isGroupInstancesModalOpen, (open) => {
if (open) {
modal.value?.show()
}
})
async function save() {
if (await saveGroupInstances()) {
modal.value?.hide()
}
}
</script>
@@ -0,0 +1,177 @@
<script setup lang="ts">
import {
ClipboardCopyIcon,
EyeIcon,
FolderOpenIcon,
MinusIcon,
PlayIcon,
PlusIcon,
StarIcon,
StopCircleIcon,
TrashIcon,
} from '@modrinth/assets'
import { computed, ref, toRef, watch } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import GroupInstancesModal from '@/components/ui/library/group-instances-modal.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 { FAVORITES_GROUP_ID } from '@/helpers/instance-groups'
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
import type { GameInstance } from '@/helpers/types'
const props = defineProps<{
instances: GameInstance[]
}>()
const {
instanceGroups,
filters,
instanceOptions,
confirmDeleteModal,
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.id === FAVORITES_GROUP_ID
? instanceGroup.instances.length > 0
: 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 }
}
function setInstanceOptions(component: unknown) {
instanceOptions.value = component as InstanceType<typeof ContextMenu> | null
}
function setConfirmDeleteModal(component: unknown) {
confirmDeleteModal.value = component as InstanceType<typeof ConfirmDeleteInstanceModal> | null
}
watch(selectedLibraryInstances, (selectedInstances) => {
if (selectedInstances.size === 0) {
anchorInstance.value = null
return
}
if (
anchorInstance.value &&
!selectedInstances.has(getLibraryInstanceSelectionKey(anchorInstance.value))
) {
anchorInstance.value = null
}
})
</script>
<template>
<InstanceGroupDnd :instances="instances">
<section data-library-page-background class="flex flex-col gap-3 pb-16">
<h2 class="m-0 text-2xl font-semibold text-contrast">Library</h2>
<LibraryToolbar />
<TransitionGroup
data-library-page-background
tag="div"
class="flex flex-col"
move-class="transition-transform duration-200 ease-out"
enter-active-class="transition-[opacity,transform] duration-200 ease-out"
enter-from-class="opacity-0 -translate-y-2"
enter-to-class="opacity-100 translate-y-0"
>
<div v-for="instanceGroup in visibleInstanceGroups" :key="instanceGroup.id" class="min-w-0">
<InstanceGroup
: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>
</TransitionGroup>
</section>
</InstanceGroupDnd>
<LibrarySelectionActionBar />
<GroupInstancesModal />
<ConfirmDeleteInstanceModal :ref="setConfirmDeleteModal" @delete="deleteInstance" />
<ContextMenu :ref="setInstanceOptions" @option-clicked="handleInstanceOption">
<template #play> <PlayIcon /> Play </template>
<template #stop> <StopCircleIcon /> Stop </template>
<template #add_to_favorites> <StarIcon /> Add to favorites </template>
<template #remove_from_favorites>
<StarIcon style="color: var(--color-text-default); fill: var(--color-text-default)" /> Remove
from favorites
</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,69 @@
<script setup lang="ts">
import { EditIcon, SquarePlusIcon, TrashIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
defineProps<{
deleting?: boolean
onAddToGroup: () => void
onDeleteGroup: () => void
onEditGroupName: () => void
}>()
const { formatMessage } = useVIntl()
const messages = defineMessages({
addToGroup: {
id: 'app.library.group.context-menu.add-to-group',
defaultMessage: 'Add instances to group',
},
editGroupName: {
id: 'app.library.group.edit-name',
defaultMessage: 'Edit group name',
},
deleteGroup: {
id: 'app.library.group.delete',
defaultMessage: 'Delete group',
},
})
</script>
<template>
<div
class="flex shrink-0 items-center opacity-0 transition-opacity duration-250 group-hover/header:opacity-100 focus-within:opacity-100"
>
<ButtonStyled circular type="transparent" size="standard">
<button
class="!size-8 !min-w-8"
v-tooltip="formatMessage(messages.editGroupName)"
type="button"
:aria-label="formatMessage(messages.editGroupName)"
@click.stop="onEditGroupName"
>
<EditIcon class="!min-w-4 !min-h-4 !size-4" />
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent" size="standard">
<button
class="!size-8 !min-w-8"
v-tooltip="formatMessage(messages.addToGroup)"
type="button"
:aria-label="formatMessage(messages.addToGroup)"
@click.stop="onAddToGroup"
>
<SquarePlusIcon class="!min-w-4 !min-h-4 !size-4" />
</button>
</ButtonStyled>
<ButtonStyled circular type="transparent" size="standard">
<button
class="!size-8 !min-w-8"
v-tooltip="formatMessage(messages.deleteGroup)"
type="button"
:aria-label="formatMessage(messages.deleteGroup)"
:disabled="deleting"
@click.stop="onDeleteGroup"
>
<TrashIcon class="!min-w-4 !min-h-4 !size-4" />
</button>
</ButtonStyled>
</div>
</template>
@@ -0,0 +1,479 @@
<script setup lang="ts">
import { useDroppable } from '@dnd-kit/vue'
import {
DropdownIcon,
EditIcon,
PlusIcon,
SquarePlusIcon,
TrashIcon,
XIcon,
} from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
commonMessages,
defineMessages,
injectNotificationManager,
InlineEditableText,
NewModal,
TagItem,
useVIntl,
} from '@modrinth/ui'
import { computed, inject, nextTick, ref, watch } from 'vue'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import GroupActionButtons from '@/components/ui/library/instance-group/group-action-buttons.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'
import { FAVORITES_GROUP_ID } from '@/helpers/instance-groups'
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,
openGroupInstancesModal,
displayState,
activeInstanceGroupDrag,
instanceGroupDragTarget,
getInstanceGroupDropState,
} = useLibrary()
const showCreationModal = inject<() => void>('showCreationModal')
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 isFavorites = computed(() => props.instanceGroup.id === FAVORITES_GROUP_ID)
const isCustomGroup = computed(
() => displayState.value.group === 'Group' && !isUngrouped.value && !isFavorites.value,
)
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',
},
favorites: {
id: 'app.library.group.favorites',
defaultMessage: 'Favorites',
},
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.',
},
newInstance: {
id: 'app.library.context-menu.create-instance',
defaultMessage: 'New instance',
},
addToGroup: {
id: 'app.library.group.context-menu.add-to-group',
defaultMessage: 'Add to group',
},
emptyGroup: {
id: 'app.library.group.empty',
defaultMessage: 'Drag and drop to add instances.',
},
})
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 || isFavorites.value) return
if (props.instanceGroup.instances.length > 0) {
confirmDeleteGroupModal.value?.show()
} else {
void removeGroup()
}
}
function openGroupContextMenu(event: MouseEvent) {
if (displayState.value.group !== 'Group') return
groupContextMenuOpen.value = true
groupOptions.value?.showMenu(
event,
props.instanceGroup,
isFavorites.value
? [{ name: 'new_instance' }]
: isCustomGroup.value
? [
{ name: 'new_instance' },
{ name: 'add_instances' },
{ name: 'edit_name' },
{ type: 'divider' },
{ name: 'delete_group', color: 'danger' },
]
: [{ name: 'new_instance' }],
)
}
function handleGroupOption({ option }: { option: string }) {
if (option === 'new_instance') {
showCreationModal?.()
return
}
if (option === 'add_instances') {
openGroupInstancesModal(props.instanceGroup.id)
return
}
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()
groupDropTarget.value?.scrollIntoView({ block: 'nearest' })
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 && !isFavorites"
ref="groupNameInput"
v-model="groupName"
activation-mode="manual"
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-if="isUngrouped"
class="text-base font-semibold text-primary select-none group-hover/open-target:text-contrast"
>
{{ formatMessage(messages.ungrouped) }}
</span>
<span
v-else
class="flex items-center gap-1.5 text-base font-semibold text-primary select-none group-hover/open-target:text-contrast"
>
{{ formatMessage(messages.favorites) }}
</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" />
<GroupActionButtons
v-if="!isUngrouped && !isFavorites"
:deleting="deletingGroup"
:on-add-to-group="() => openGroupInstancesModal(instanceGroup.id)"
:on-delete-group="requestGroupDeletion"
:on-edit-group-name="() => groupNameInput?.startEditing()"
/>
</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"
>
{{ formatMessage(messages.emptyGroup) }}
</p>
</section>
</Accordion>
</div>
<ContextMenu
ref="groupOptions"
@menu-closed="groupContextMenuOpen = false"
@option-clicked="handleGroupOption"
>
<template #new_instance> <PlusIcon /> {{ formatMessage(messages.newInstance) }} </template>
<template #add_instances>
<SquarePlusIcon /> {{ formatMessage(messages.addToGroup) }}
</template>
<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,331 @@
<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 { useEventListener, 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 isPrimaryPointerDown = 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 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 handlePointerDown = (event: PointerEvent) => {
isPrimaryPointerDown.value =
event.button === 0 &&
!(event.target instanceof Element && event.target.closest('.selection-button'))
}
const resetPrimaryPointer = () => {
isPrimaryPointerDown.value = false
}
useEventListener(window, 'pointerup', resetPrimaryPointer)
useEventListener(window, 'pointercancel', resetPrimaryPointer)
useEventListener(window, 'blur', resetPrimaryPointer)
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,
'scale-[0.95]': isPrimaryPointerDown && !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"
@pointerdown="handlePointerDown"
>
<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>
File diff suppressed because it is too large Load Diff
@@ -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,24 @@
import { invoke } from '@tauri-apps/api/core'
export const FAVORITES_GROUP_ID = 'group:favorites'
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,
],
() => {
+58 -87
View File
@@ -1,30 +1,39 @@
<script setup lang="ts">
import { injectNotificationManager } from '@modrinth/ui'
import type { SearchResult } from '@modrinth/utils'
import { PlusIcon } from '@modrinth/assets'
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
import dayjs from 'dayjs'
import { computed, onUnmounted, ref } from 'vue'
import { computed, inject, onUnmounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import RowDisplay from '@/components/RowDisplay.vue'
import ContextMenu from '@/components/ui/ContextMenu.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 { formatMessage } = useVIntl()
const { hasCreatedInstance, isReady } = injectOnboardingChecklist()
const route = useRoute()
const breadcrumbs = useBreadcrumbs()
const showCreationModal = inject<() => void>('showCreationModal')
const pageOptions = ref<InstanceType<typeof ContextMenu>>()
const messages = defineMessages({
newInstance: {
id: 'app.library.context-menu.create-instance',
defaultMessage: 'New instance',
},
})
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,96 +41,58 @@ 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()
})
function openPageContextMenu(event: MouseEvent) {
if (
!(event.target instanceof HTMLElement) ||
!event.target.hasAttribute('data-library-page-background')
) {
return
}
event.preventDefault()
event.stopPropagation()
pageOptions.value?.showMenu(event, {}, [{ name: 'new_instance' }])
}
function handlePageOption({ option }: { option: string }) {
if (option === 'new_instance') {
showCreationModal?.()
}
}
</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"
data-library-page-background
class="flex flex-col gap-6 p-6"
@contextmenu="openPageContextMenu"
>
<RecentWorldsList v-if="recentInstances?.length > 0" :recent-instances="recentInstances" />
<LibrarySection :instances="instances" />
<ContextMenu ref="pageOptions" @option-clicked="handlePageOption">
<template #new_instance>
<PlusIcon /> {{ formatMessage(messages.newInstance) }}
</template>
</ContextMenu>
</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,

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