Compare commits

...
Author SHA1 Message Date
Calum H. (IMB11) 8b4f913605 feat: qa 2026-05-23 21:35:37 +01:00
Calum H. (IMB11) 8e61f042b7 feat: qa + app routing bugs 2026-05-23 21:09:18 +01:00
Calum H. (IMB11) 8301620b76 feat: migrate all headers to use PageHeader shared component. 2026-05-23 19:12:46 +01:00
Calum H. (IMB11) 3a69e8818a fix: ssr for instances subpages with middleware 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 8944437f1b fix: ssr 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 65e250f7f2 fix: header power state 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 5e34efea22 feat: header migration pt 2 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) e36f8a42f5 feat: PageHeader migration start 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) c5f1fa1154 fix: header issues 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 443e17f9d5 fix: lint 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 7622abe59b qa: pass 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 76036602a3 chore: rename worlds -> instances 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) e2ac1274ac fix: modpack linked text alignment 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) ceef2811f4 fix: worlds layout 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 739a1baef5 feat: fix btn sizing 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) 1e1a75ab90 fix: lint 2026-05-23 19:09:14 +01:00
Calum H. (IMB11) b6f25e9ed4 chore: clean up wrapped layout folder structure 2026-05-23 19:07:40 +01:00
Calum H. (IMB11) ef4c2e3161 feat: reshuffle layout for worlds 2026-05-23 19:05:31 +01:00
Calum H. (IMB11) 7911ce8e4a feat: world card alignment 2026-05-23 19:02:50 +01:00
Calum H. (IMB11) fcaaf534a8 fix: lint + i18n 2026-05-23 19:02:50 +01:00
Calum H. (IMB11) 3cd18b7205 feat: world switching page implementation 2026-05-23 19:02:50 +01:00
f9d47e8edc feat: Add new "Enabled" sorting button next to "Disabled" (#6000)
* feat: Added new "Enabled" sorting button next to "Disabled"

* Updated when filter buttons show

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

* Updated when filter buttons show

---------

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

* Migrate composable to ts

* Migrate to ButtonStyled, fix coloring

* Fix lint

* Fix clearing java path not refreshing state

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

---------

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

* fix

* post-query ua bucketing

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

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

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

Closes: #6154

* fix: further regressions

* fix: lint

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

* make it a tristate

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

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

* fix: files not scoped

* feat: hide staging files folder and proper cancel feedback

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

* wip: occlusion works

* occlusion notification

* fix ci

* fix

* wire in hiding into macos occlusion

* remove debug logs

* fix

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

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

* feat: update date picker to analytics branch changes

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

* fix: positioning size + css transition

* fix: lint

* fix: ts

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

* fix: pending install state loss

* fix: mods.vue perf problems

* chore: todo doc

* draft: try preload/fix suspense

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

* more

* revert server

* clean names

* prepr
2026-05-19 18:11:48 +00:00
aecsocketandGitHub f106dc580f Restrict what tokens can be used for auth init flows (#6137) 2026-05-19 15:45:58 +00:00
aecsocketandGitHub 244c263e40 Implement analytics marker events (#6090)
* Analytics events

* prepare

* change route prefix

* update route return

* Add mod launcher analytics

* more UA strings

* fix ci

* caching on analytics events

* Return parent modpack versions for playtime queries

* sqlx prepare

* fmt

* dummy fixtures
2026-05-19 13:06:04 +00:00
48bb44155d New translations from Crowdin (main) (#6127)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-18 14:53:39 +00:00
Calum H.andGitHub 3f2e76ae7e fix: table i18n bug (#6129) 2026-05-18 14:53:25 +00:00
6479eca0e9 fix: pix withdrawals (#6128)
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-18 14:51:55 +00:00
Calum H.andGitHub 8767bc9184 fix: changelog lint (#6130)
fix: changelog
2026-05-18 14:46:31 +00:00
aecsocketandGitHub d1185414b6 Fix changelog (#6125)
fix changelog
2026-05-17 23:21:10 +02:00
aecsocketandGitHub 510439acbf Changelog (#6124)
changelog
2026-05-17 22:50:23 +02:00
aecsocketandGitHub c564495e11 Ad webview occlusion handling (#6116)
* wip: ad webview occlusion

* Ad webview window occlusion testing

* revert refresh test
2026-05-17 20:27:04 +00:00
simonLeary42andGitHub 8dd1490c8a disable Stable loader version type if there are no stable loader versions available (#6114)
* disable Stable loader version if there are no stable loader versions

* deselect stable if stable is disabled
2026-05-16 17:07:00 +00:00
François-Xavier TalbotandGitHub b72bc18a6b Add moderator notes to users & organizations (#6094)
* Moderator notes

* Use macros

* Improve queries

* Query cache

* Accept missing If-Match if no existing note

* Undo v2 compat changes

* Fix tests

* Remove CONSTRAINT CHECK on moderation_notes

* Respect 1-indexing on moderation_notes.version default in DB migration

* Remove double Option

* .body("") -> .finish()

* .remove() -> .get().clone()

* cloned

* Review comments

* moderation_notes everywhere
2026-05-16 16:30:36 +00:00
aecsocketandGitHub cee942dcef Fix file version updates not considering visibility (#6105) 2026-05-16 16:18:18 +00:00
02a7774722 fix: add download attribute to fix JAR files saving as ZIP in Chromium (#6065)
* fix: add download attribute to fix JAR files saving as ZIP in Chromium

- JAR files were downloading with a `.zip` extension in Chromium-based browsers (Chrome, Edge, Arc, Brave, Opera, Vivaldi)
- Root cause: JAR files are ZIP archives internally, so Chromium sniffs the `Content-Type` as `application/zip` and overrides the filename extension when no `download` attribute is present
- Fix: add `download="<filename>"` to all file download `<a>` tags so the browser uses the original filename from the API

* fix: add download attribute to remaining download links

Missed in initial pass: changelog page button, versions overflow
menu, settings/versions overflow menu. Also adds `download` prop
to Button and OverflowMenu to support dropdown link items.

Adds missing `getPrimaryFile` definition in changelog.vue.

---------

Co-authored-by: Mr_chank <180248271+chank-op@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2026-05-15 14:58:26 +00:00
François-Xavier TalbotandGitHub e9eb98f97e push: true (#6096) 2026-05-14 00:54:37 +02:00
aecsocketandGitHub e5bbd9d409 Remove non-Typesense search backends, add default env vars (#6082)
* Remove non-Typesense search backends, add default env vars

* shear

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

* make requested changes

* add boolean for showing delay message

* fix server icon + shortened code

* fix server icon

* refactor admonitions

* msg correction.

* correction + change spam-notice

* Separate status info from instruction details

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

* Whoops, actually updated msgs correctly now.

* prepr + margin

* split out strings, simplify code again

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

* fix: move tooltip to button

* fix: lock status buttons after pressing

* fix: unlisted/withheld icon on legacy badge

* prepprrr

* fix banners, add some extra dev mode stuff

* fix thread id copy padding

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

* update messages & bunch of other stuff

* rename toggle

* change hover to 2.5, fix error size

* private msg overlay

---------

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

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

* add logging and fix tauri variable

* use state

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

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

* Remove unnecessary styling

* Only expand width when required

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

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

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

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

* feat: improve add dependency flow

* feat: update suggested dependency style

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

* feat: implement combobox select searched text on focus

* feat: add Tabs.vue

* feat: update nav tabs to use tabs

* feat: improve project search dropdown

* fix: dependency search not clearing inbound query

* fix: combobox no options open state bug

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

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

* tweak agents

* factor tags into its own util

* lock cache refresh to avoid cache stampede

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

* cargo prepare
2026-05-10 10:57:24 +00:00
412 changed files with 27714 additions and 9133 deletions
@@ -9,8 +9,6 @@ body:
options:
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate feature requests
required: true
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
required: true
- type: dropdown
id: projects
attributes:
+1 -1
View File
@@ -164,7 +164,7 @@ jobs:
with:
context: ./apps/labrinth/docker-stage
file: ./apps/labrinth/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
annotations: ${{ steps.docker-meta.outputs.annotations }}
Generated
+4 -24
View File
@@ -3059,29 +3059,6 @@ dependencies = [
"serde",
]
[[package]]
name = "elasticsearch"
version = "9.1.0-alpha.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12bb303aa6e1d28c0c86b6fbfe484fd0fd3f512629aeed1ac4f6b85f81d9834a"
dependencies = [
"base64 0.22.1",
"bytes",
"dyn-clone",
"flate2",
"lazy_static",
"parking_lot",
"percent-encoding",
"reqwest 0.12.24",
"rustc_version",
"serde",
"serde_json",
"serde_with",
"tokio",
"url",
"void",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
@@ -5275,7 +5252,6 @@ dependencies = [
"dotenv-build",
"dotenvy",
"either",
"elasticsearch",
"eyre",
"futures",
"futures-util",
@@ -10589,6 +10565,8 @@ version = "1.0.0-local"
dependencies = [
"async_zip",
"chrono",
"core-foundation 0.10.1",
"core-graphics",
"daedalus",
"dashmap",
"either",
@@ -10596,6 +10574,7 @@ dependencies = [
"hyper 1.7.0",
"hyper-util",
"native-dialog",
"objc2-app-kit",
"paste",
"path-util",
"serde",
@@ -10621,6 +10600,7 @@ dependencies = [
"urlencoding",
"uuid 1.18.1",
"webview2-com",
"windows",
"windows-core 0.61.2",
]
+3 -1
View File
@@ -64,6 +64,8 @@ clickhouse = "0.14.0"
color-eyre = "0.6.5"
color-thief = "0.2.2"
const_format = "0.2.34"
core-foundation = "0.10.1"
core-graphics = "0.24.0"
daedalus = { path = "packages/daedalus" }
darling = { version = "0.23" }
dashmap = "6.1.0"
@@ -77,7 +79,6 @@ dotenv-build = "0.1.1"
dotenvy = "0.15.7"
dunce = "1.0.5"
either = "1.15.0"
elasticsearch = "9.1.0-alpha.1"
encoding_rs = "0.8.35"
enumset = "1.1.10"
eyre = "0.6.12"
@@ -124,6 +125,7 @@ murmur2 = "0.1.0"
native-dialog = "0.9.2"
notify = { version = "8.2.0", default-features = false }
notify-debouncer-mini = { version = "0.7.0", default-features = false }
objc2-app-kit = { version = "0.3.2", default-features = false }
p256 = "0.13.2"
parking_lot = "0.12.5"
paste = "1.0.15"
+1 -2
View File
@@ -13,14 +13,13 @@
"test": "vue-tsc --noEmit"
},
"dependencies": {
"@intercom/messenger-js-sdk": "^0.0.14",
"@modrinth/api-client": "workspace:^",
"@modrinth/assets": "workspace:*",
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/vue": "^8.27.0",
"@sfirew/minecraft-motd-parser": "^1.1.6",
"@tanstack/vue-query": "^5.90.7",
"@tanstack/vue-query": "5.90.7",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.4.5",
+155 -98
View File
@@ -1,7 +1,7 @@
<script setup>
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
import {
AuthFeature,
ModrinthApiError,
NodeAuthFeature,
nodeAuthState,
PanelVersionFeature,
@@ -52,6 +52,7 @@ import {
providePopupNotificationManager,
useDebugLogger,
useFormatBytes,
useHostingIntercom,
useVIntl,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
@@ -87,7 +88,6 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useIntercomPositioning } from '@/composables/intercom-positioning'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import { config } from '@/config'
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
@@ -98,6 +98,7 @@ import { command_listener, warning_listener } from '@/helpers/events.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack'
import { list } from '@/helpers/profile.js'
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
import {
@@ -107,6 +108,7 @@ import {
getUpdateSize,
isDev,
isNetworkMetered,
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import i18n from '@/i18n.config'
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
@@ -129,15 +131,53 @@ import { AppPopupNotificationManager } from './providers/app-popup-notifications
const themeStore = useTheming()
const router = useRouter()
const route = useRoute()
const intercomBubblePositioning = useIntercomPositioning({ route, themeStore })
const {
sidebarToggled,
forceSidebar,
sidebarVisible,
intercomBubblePosition,
updateIntercomBubbleStyles,
clearIntercomBubbleStyles,
} = intercomBubblePositioning
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const ROUTE_SUSPENSE_TIMEOUT_MS = 60_000
const credentials = ref()
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const keepAliveRouteComponents = computed(() => [
...new Set(
router
.getRoutes()
.map((route) => route.meta.keepAliveComponent)
.filter((name) => typeof name === 'string'),
),
])
function getRouteViewKey(viewRoute) {
const keepAliveKey = viewRoute.meta.keepAliveKey
if (typeof keepAliveKey === 'function') return keepAliveKey(viewRoute)
if (typeof keepAliveKey === 'string') return keepAliveKey
return undefined
}
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const hostingIntercomIdentityKey = computed(() => {
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
const userId = credentials.value?.user_id ?? credentials.value?.user?.id ?? 'anonymous'
return `${userId}:${serverId ?? 'hosting'}`
})
const hostingIntercom = useHostingIntercom({
enabled: computed(() => hostingRouteActive.value && !!credentials.value?.session),
appId: 'ykeritl9',
fetchToken: fetchIntercomToken,
identityKey: hostingIntercomIdentityKey,
horizontalPadding: computed(() =>
sidebarVisible.value
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
: INTERCOM_BUBBLE_DEFAULT_PADDING,
),
})
const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
@@ -172,7 +212,11 @@ provideModrinthClient(tauriApiClient)
providePageContext({
hierarchicalSidebarAvailable: ref(true),
showAds: ref(false),
...intercomBubblePositioning.pageContext,
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: hostingIntercom.intercomBubble,
featureFlags: {
serverRamAsBytesAlwaysOn: computed(() =>
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
@@ -256,8 +300,7 @@ onMounted(async () => {
onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
shutdownHostingIntercom()
clearIntercomBubbleStyles()
unsubscribeSidebarToggle()
await unlistenUpdateDownload?.()
})
@@ -456,6 +499,11 @@ const sidebarOverlayScrollbarsOptions = Object.freeze({
},
})
router.beforeEach(async (to) => {
const redirect = await resolveLegacyServerInstanceTabRedirect(to)
if (redirect) return redirect
})
router.beforeEach(() => {
suspensePending = false
if (routerToken) loading.end(routerToken)
@@ -487,6 +535,50 @@ function onSuspensePending() {
suspenseToken = loading.begin()
}
async function resolveLegacyServerInstanceTabRedirect(to) {
if (!['ServerManageContent', 'ServerManageFiles', 'ServerManageBackups'].includes(to.name)) {
return null
}
const serverId = getRouteParam(to.params.id)
if (!serverId) return null
const tabPath =
to.name === 'ServerManageFiles' ? '/files' : to.name === 'ServerManageBackups' ? '/backups' : ''
const instancesPath = `/hosting/manage/${encodeURIComponent(serverId)}/instances`
try {
const serverFull = await tauriApiClient.archon.servers_v1.get(serverId)
const world = serverFull.worlds.find((item) => item.is_active) ?? serverFull.worlds[0]
if (world) {
return {
path: `${instancesPath}/${encodeURIComponent(world.id)}${tabPath}`,
query: to.query,
hash: to.hash,
replace: true,
}
}
} catch {
return {
path: instancesPath,
query: to.query,
hash: to.hash,
replace: true,
}
}
return {
path: instancesPath,
query: to.query,
hash: to.hash,
replace: true,
}
}
function getRouteParam(param) {
return Array.isArray(param) ? param[0] : param
}
function onSuspenseResolve() {
if (suspenseToken) {
loading.end(suspenseToken)
@@ -590,8 +682,6 @@ const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const updateToPlayModal = ref()
const credentials = ref()
const modrinthLoginFlowWaitModal = ref()
setupAuthProvider(credentials, async (_redirectPath) => {
@@ -661,10 +751,6 @@ const hasPlus = computed(
const showAd = computed(
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
let intercomBooting = false
let intercomBooted = false
async function fetchIntercomToken() {
const creds = await getCreds()
@@ -673,8 +759,10 @@ async function fetchIntercomToken() {
}
const params = new URLSearchParams()
if (route.path.startsWith('/hosting/manage/') && typeof route.params.id === 'string') {
params.set('server_id', route.params.id)
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
if (route.path.startsWith('/hosting/manage/') && typeof serverId === 'string') {
params.set('server_id', serverId)
}
const query = params.size > 0 ? `?${params.toString()}` : ''
@@ -690,69 +778,6 @@ async function fetchIntercomToken() {
return await response.json()
}
async function bootIntercom() {
if (
intercomBooting ||
intercomBooted ||
!hostingRouteActive.value ||
!credentials.value?.session
) {
return
}
intercomBooting = true
console.debug('[APP][INTERCOM] initializing secure support chat')
try {
const { token } = await fetchIntercomToken()
Intercom({
app_id: 'ykeritl9',
intercom_user_jwt: token,
session_duration: 1000 * 60 * 60 * 24,
alignment: 'right',
horizontal_padding: intercomBubblePosition.value.horizontalPadding,
vertical_padding: intercomBubblePosition.value.verticalPadding,
})
intercomBooted = true
} catch (error) {
console.warn('[APP][INTERCOM] failed to initialize secure support chat', error)
} finally {
intercomBooting = false
}
}
function shutdownHostingIntercom() {
if (!intercomBooted && !intercomBooting) return
shutdownIntercom()
intercomBooting = false
intercomBooted = false
}
watch(
intercomBubblePosition,
(position) => {
updateIntercomBubbleStyles(position)
if (intercomBooted) {
window.Intercom?.('update', {
horizontal_padding: position.horizontalPadding,
vertical_padding: position.verticalPadding,
})
}
},
{ immediate: true },
)
watch(
[hostingRouteActive, credentials],
([active]) => {
if (active) {
void bootIntercom()
} else {
shutdownHostingIntercom()
}
},
{ immediate: true },
)
watch(showAd, () => {
if (!showAd.value) {
hide_ads_window(true)
@@ -1023,11 +1048,40 @@ async function downloadUpdate(versionToDownload) {
async function installUpdate() {
restarting.value = true
try {
await setRestartAfterPendingUpdate(true)
} catch (e) {
restarting.value = false
handleError(e)
return
}
setTimeout(async () => {
await handleClose()
}, 250)
}
async function openModrinthProjectLinkInApp(parsed) {
const { slug, pathSuffix, url } = parsed
const loadToken = loading.begin()
try {
const { id } = await tauriApiClient.labrinth.projects_v2.check(slug)
const query = mergeUrlQuery(route.query, url)
await router.push({
path: `/project/${id}${pathSuffix}`,
query,
hash: url.hash || undefined,
})
} catch (err) {
if (err instanceof ModrinthApiError && err.statusCode === 404) {
openUrl(url.href)
} else {
handleError(err)
}
} finally {
loading.end(loadToken)
}
}
function handleClick(e) {
let target = e.target
while (target != null) {
@@ -1040,7 +1094,12 @@ function handleClick(e) {
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
) {
openUrl(target.href)
const parsed = parseModrinthLink(target.href)
if (target.target !== '_blank' && parsed) {
void openModrinthProjectLinkInApp(parsed)
} else {
openUrl(target.href)
}
}
e.preventDefault()
break
@@ -1453,11 +1512,17 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
{{ formatMessage(messages.authUnreachableBody) }}
</Admonition>
<RouterView v-slot="{ Component }">
<RouterView v-slot="{ Component, route: viewRoute }">
<template v-if="Component">
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
<component :is="Component"></component>
</Suspense>
<KeepAlive :include="keepAliveRouteComponents" :max="3">
<Suspense
:timeout="ROUTE_SUSPENSE_TIMEOUT_MS"
@pending="onSuspensePending"
@resolve="onSuspenseResolve"
>
<component :is="Component" :key="getRouteViewKey(viewRoute)"></component>
</Suspense>
</KeepAlive>
</template>
</RouterView>
</div>
@@ -1785,14 +1850,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
--os-handle-bg-active: var(--color-scrollbar) !important;
}
.intercom-lightweight-app-launcher,
.intercom-launcher-frame,
iframe[name='intercom-launcher-frame'] {
right: var(--app-support-launcher-right, 20px) !important;
bottom: var(--app-support-launcher-bottom, 20px) !important;
z-index: 9 !important;
}
.mac {
.app-grid-statusbar {
padding-left: 5rem;
@@ -1,35 +1,33 @@
<template>
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
<div class="auto-detect-modal">
<div class="table">
<div class="table-row table-head">
<div class="table-cell table-text">Version</div>
<div class="table-cell table-text">Path</div>
<div class="table-cell table-text">Actions</div>
</div>
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
<div class="table-cell table-text">
<span>{{ javaInstall.version }}</span>
</div>
<div v-tooltip="javaInstall.path" class="table-cell table-text">
<span>{{ javaInstall.path }}</span>
</div>
<div class="table-cell table-text manage">
<ButtonStyled v-if="currentSelected.path === javaInstall.path">
<button disabled><CheckIcon /> Selected</button>
<div class="flex flex-col gap-4">
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
<template #cell-version="{ value }">
<span class="font-semibold text-primary">{{ value }}</span>
</template>
<template #cell-path="{ value }">
<span v-tooltip="value" class="block truncate font-mono text-xs">{{ value }}</span>
</template>
<template #cell-actions="{ row }">
<div class="flex items-center justify-end">
<ButtonStyled v-if="currentSelected.path === row.path">
<button class="!shadow-none" disabled><CheckIcon /> Selected</button>
</ButtonStyled>
<ButtonStyled v-else>
<button @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</button>
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
</ButtonStyled>
</div>
</div>
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
<div class="table-cell table-text">No java installations found!</div>
</div>
</div>
<div class="input-group push-right">
</template>
<template #empty-state>
<div class="p-4 text-secondary">No java installations found!</div>
</template>
</Table>
<div class="flex justify-end">
<ButtonStyled type="outlined">
<button @click="$refs.detectJavaModal.hide()">
<button
class="!shadow-none !border-surface-4 !border"
@click="$refs.detectJavaModal.hide()"
>
<XIcon />
Cancel
</button>
@@ -40,7 +38,7 @@
</template>
<script setup>
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
@@ -52,6 +50,11 @@ const { handleError } = injectNotificationManager()
const chosenInstallOptions = ref([])
const detectJavaModal = ref(null)
const currentSelected = ref({})
const javaInstallColumns = [
{ key: 'version', label: 'Version', width: '9rem' },
{ key: 'path', label: 'Path' },
{ key: 'actions', label: 'Actions', align: 'right', width: '10rem' },
]
defineExpose({
show: async (version, currentSelectedJava) => {
@@ -77,25 +80,3 @@ function setJavaInstall(javaInstall) {
})
}
</script>
<style lang="scss" scoped>
.auto-detect-modal {
.table {
.table-row {
grid-template-columns: 1fr 4fr min-content;
}
span {
display: inherit;
align-items: center;
justify-content: center;
}
padding: 0.5rem;
}
}
.manage {
display: flex;
gap: 0.5rem;
}
</style>
@@ -1,85 +1,104 @@
<template>
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
<div class="toggle-setting" :class="{ compact }">
<StyledInput
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
wrapper-class="installation-input"
@update:model-value="
(val) => {
emit('update:modelValue', {
...props.modelValue,
path: val,
})
}
"
/>
<div :id="props.id" class="toggle-setting" :class="{ compact }">
<div class="input-with-status">
<StyledInput
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
wrapper-class="installation-input"
@update:model-value="
(val) => {
emit('update:modelValue', {
...props.modelValue,
path: val,
})
}
"
/>
<ButtonStyled
:color="
!hoveringTest && !testingJava
? testingJavaSuccess === true
? 'green'
: 'red'
: 'standard'
"
color-fill="text"
>
<button
class="!shadow-none"
:disabled="testingJava || props.disabled"
@click="runTest(props.modelValue?.path)"
@mouseenter="!props.disabled && (hoveringTest = true)"
@mouseleave="hoveringTest = false"
>
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
<CheckCircleIcon
v-else-if="testingJavaSuccess === true && !hoveringTest"
class="h-4 w-4"
/>
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
</button>
</ButtonStyled>
</div>
<span class="installation-buttons">
<ButtonStyled v-if="props.version">
<button :disabled="props.disabled || installingJava" @click="reinstallJava">
<button
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
class="!shadow-none"
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
@click="reinstallJava"
>
<DownloadIcon />
{{ installingJava ? 'Installing...' : 'Install recommended' }}
</button>
</ButtonStyled>
<ButtonStyled>
<button :disabled="props.disabled" @click="autoDetect">
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
<SearchIcon />
Detect
</button>
</ButtonStyled>
<ButtonStyled>
<button :disabled="props.disabled" @click="handleJavaFileInput()">
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
<FolderSearchIcon />
Browse
</button>
</ButtonStyled>
<ButtonStyled v-if="testingJava">
<button disabled>Testing...</button>
</ButtonStyled>
<ButtonStyled v-else-if="testingJavaSuccess === true">
<button disabled>
<CheckIcon />
Success
</button>
</ButtonStyled>
<ButtonStyled v-else-if="testingJavaSuccess === false">
<button disabled>
<XIcon />
Failed
</button>
</ButtonStyled>
<ButtonStyled v-else>
<button :disabled="props.disabled" @click="testJava">
<PlayIcon />
Test
</button>
</ButtonStyled>
</span>
</div>
</template>
<script setup>
import {
CheckIcon,
CheckCircleIcon,
DownloadIcon,
FolderSearchIcon,
PlayIcon,
RefreshCwIcon,
SearchIcon,
XIcon,
SpinnerIcon,
XCircleIcon,
} from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref } from 'vue'
import { ref, watch } from 'vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import { trackEvent } from '@/helpers/analytics'
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager()
const props = defineProps({
id: {
type: String,
required: false,
default: null,
},
version: {
type: Number,
required: false,
@@ -110,29 +129,36 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const testingJava = ref(false)
const testingJavaSuccess = ref(null)
const {
testingJava,
javaTestResult: testingJavaSuccess,
testJavaInstallationDebounced,
testJavaInstallation,
} = useJavaTest()
const installingJava = ref(false)
const hoveringTest = ref(false)
let hasInitialized = false
async function testJava() {
testingJava.value = true
testingJavaSuccess.value = await test_jre(
props.modelValue ? props.modelValue.path : '',
props.version,
)
testingJava.value = false
trackEvent('JavaTest', {
path: props.modelValue ? props.modelValue.path : '',
success: testingJavaSuccess.value,
})
setTimeout(() => {
testingJavaSuccess.value = null
}, 2000)
async function runTest(path) {
await testJavaInstallation(path, props.version, true)
}
watch(
() => props.modelValue?.path,
(newPath) => {
if (newPath) {
if (!hasInitialized) {
testJavaInstallation(newPath, props.version, false)
hasInitialized = true
} else {
testJavaInstallationDebounced(newPath, props.version)
}
}
},
{ immediate: true },
)
async function handleJavaFileInput() {
const filePath = await open()
@@ -142,6 +168,7 @@ async function handleJavaFileInput() {
result = {
path: filePath.path ?? filePath,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -175,6 +202,7 @@ async function reinstallJava() {
result = {
path: path,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -186,13 +214,23 @@ async function reinstallJava() {
emit('update:modelValue', result)
installingJava.value = false
runTest(result.path)
}
</script>
<style lang="scss" scoped>
.input-with-status {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
width: 100%;
min-width: 0;
}
.installation-input {
width: 100% !important;
flex-grow: 1;
flex: 1 1 0;
min-width: 0;
}
.toggle-setting {
@@ -215,12 +253,4 @@ async function reinstallJava() {
gap: 0.5rem;
margin: 0;
}
.test-success {
color: var(--color-green);
}
.test-fail {
color: var(--color-red);
}
</style>
@@ -37,6 +37,6 @@ defineProps({
.progress-bar__fill {
height: 100%;
transition: width 0.3s;
transition: width 0.3s ease-out;
}
</style>
@@ -126,7 +126,7 @@ watch(
function fakeLoadingIncrease() {
if (loadingProgress.value < 95) {
setTimeout(() => {
loadingProgress.value += 1
loadingProgress.value += 2
fakeLoadingIncrease()
}, 5)
}
@@ -22,7 +22,7 @@
hover-color-fill="background"
circular
>
<button class="relative expanded-button" @click="handleClose">
<button class="relative expanded-button close-button" @click="handleClose">
<XIcon />
</button>
</ButtonStyled>
@@ -82,8 +82,12 @@ const handleClose = async () => {
</script>
<style scoped>
.expanded-button::before {
inset: -6px;
inset: -9px -6px;
content: '';
position: absolute;
}
.expanded-button.close-button::before {
inset: -9px -9px -9px -6px;
}
</style>
@@ -35,7 +35,7 @@ const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const { instance, offline, isMinecraftServer, onUnlinked } = injectInstanceSettings()
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const [
fabric_versions,
@@ -113,6 +113,7 @@ provideAppBackup({
})
provideInstallationSettings({
closeSettings: closeModal,
loading: ref(false),
installationInfo: computed(() => {
const rows = [
@@ -1,6 +1,15 @@
<script setup lang="ts">
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
import {
CheckCircleIcon,
CoffeeIcon,
FolderSearchIcon,
RefreshCwIcon,
SearchIcon,
SpinnerIcon,
XCircleIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
defineMessages,
injectNotificationManager,
@@ -8,9 +17,11 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, readonly, ref, watch } from 'vue'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
@@ -25,9 +36,54 @@ const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
const overrideJavaInstall = ref(!!instance.value.java_path)
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? instance.value.java_path })
const overrideJavaInstall = ref(!!instance.value.java_path)
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
const activePath = computed(() =>
overrideJavaInstall.value ? javaPath.value : (optimalJava?.path ?? ''),
)
watch(overrideJavaInstall, (enabled) => {
if (enabled && !javaPath.value) {
javaPath.value = optimalJava?.path ?? ''
}
})
const { testingJava, javaTestResult, testJavaInstallationDebounced, testJavaInstallation } =
useJavaTest()
const hoveringTest = ref(false)
let hasInitialized = false
watch(
activePath,
(newPath) => {
if (newPath && optimalJava?.parsed_version) {
if (!hasInitialized) {
testJavaInstallation(newPath, optimalJava?.parsed_version, false)
hasInitialized = true
} else {
testJavaInstallationDebounced(newPath, optimalJava?.parsed_version)
}
}
},
{ immediate: true },
)
const javaDetectionModal = ref<{ show: (version: number, current: object) => void } | null>(null)
async function handleBrowseJava() {
const result = await open({ multiple: false })
if (result) {
javaPath.value = result
}
}
function handleDetectJava() {
javaDetectionModal.value?.show(optimalJava?.parsed_version, { path: javaPath.value })
}
const overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0)
const javaArgs = ref(
@@ -51,8 +107,8 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
const editProfileObject = computed(() => {
return {
java_path:
overrideJavaInstall.value && javaInstall.value.path !== ''
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
overrideJavaInstall.value && javaPath.value
? javaPath.value.replace('java.exe', 'javaw.exe')
: null,
extra_launch_args: overrideJavaArgs.value
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
@@ -71,7 +127,7 @@ const editProfileObject = computed(() => {
watch(
[
overrideJavaInstall,
javaInstall,
javaPath,
overrideJavaArgs,
javaArgs,
overrideEnvVars,
@@ -90,17 +146,45 @@ const messages = defineMessages({
id: 'instance.settings.tabs.java.java-installation',
defaultMessage: 'Java installation',
},
customJavaInstallation: {
id: 'instance.settings.tabs.java.custom-java-installation',
defaultMessage: 'Custom Java installation',
},
javaPathPlaceholder: {
id: 'instance.settings.tabs.java.java-path-placeholder',
defaultMessage: '/path/to/java',
},
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
},
customMemoryAllocation: {
id: 'instance.settings.tabs.java.custom-memory-allocation',
defaultMessage: 'Custom memory allocation',
},
javaArguments: {
id: 'instance.settings.tabs.java.java-arguments',
defaultMessage: 'Java arguments',
},
customJavaArguments: {
id: 'instance.settings.tabs.java.custom-java-arguments',
defaultMessage: 'Custom Java arguments',
},
enterJavaArguments: {
id: 'instance.settings.tabs.java.enter-java-arguments',
defaultMessage: 'Enter Java arguments...',
},
javaEnvironmentVariables: {
id: 'instance.settings.tabs.java.environment-variables',
defaultMessage: 'Environment variables',
},
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
customEnvironmentVariables: {
id: 'instance.settings.tabs.java.custom-environment-variables',
defaultMessage: 'Custom environment variables',
},
enterEnvironmentVariables: {
id: 'instance.settings.tabs.java.enter-environment-variables',
defaultMessage: 'Enter environmental variables...',
},
hooks: {
id: 'instance.settings.tabs.java.hooks',
@@ -111,43 +195,86 @@ const messages = defineMessages({
<template>
<div>
<h2 id="project-name" class="m-0 mb-2.5 text-lg font-semibold text-contrast block">
<JavaDetectionModal ref="javaDetectionModal" @submit="(val) => (javaPath = val.path)" />
<h2 class="m-0 mb-2 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaInstallation) }}
</h2>
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2.5" />
<template v-if="!overrideJavaInstall">
<div class="flex my-2 items-center gap-2 font-semibold">
<template v-if="javaInstall">
<CheckCircleIcon class="text-brand-green h-4 w-4" />
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
</template>
<template v-else-if="optimalJava">
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
one below:</span
<Checkbox
v-model="overrideJavaInstall"
:label="formatMessage(messages.customJavaInstallation)"
class="mb-2"
/>
<div class="flex gap-4 p-4 bg-bg rounded-2xl">
<div class="flex gap-3 items-start flex-1 min-w-0">
<div
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 mt-1 shrink-0 [&_svg]:h-full [&_svg]:w-full"
>
<CoffeeIcon />
</div>
<div class="flex flex-col gap-2 flex-1 min-w-0">
<span class="font-semibold leading-none mt-2"
>Java {{ optimalJava?.parsed_version }}</span
>
</template>
<template v-else>
<XCircleIcon class="text-brand-red h-5 w-5" />
<span
>Could not automatically determine a Java installation to use. Please set one
below:</span
>
</template>
<div class="flex gap-2 items-center">
<StyledInput
:model-value="activePath"
:disabled="!overrideJavaInstall"
autocomplete="off"
:placeholder="formatMessage(messages.javaPathPlaceholder)"
wrapper-class="flex-1 min-w-0"
@update:model-value="(val) => (javaPath = String(val))"
/>
<ButtonStyled
:color="
!hoveringTest && !testingJava
? javaTestResult === true
? 'green'
: 'red'
: 'standard'
"
color-fill="text"
>
<button
:disabled="!overrideJavaInstall || testingJava"
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
@mouseenter="overrideJavaInstall && (hoveringTest = true)"
@mouseleave="hoveringTest = false"
>
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
<CheckCircleIcon
v-else-if="javaTestResult === true && !hoveringTest"
class="h-4 w-4"
/>
<XCircleIcon v-else-if="javaTestResult !== true && !hoveringTest" class="h-4 w-4" />
<RefreshCwIcon v-else-if="overrideJavaInstall" class="h-4 w-4" />
</button>
</ButtonStyled>
</div>
<div v-if="overrideJavaInstall" class="flex gap-2">
<ButtonStyled>
<button @click="handleDetectJava">
<SearchIcon />
Detect
</button>
</ButtonStyled>
<ButtonStyled>
<button @click="handleBrowseJava">
<FolderSearchIcon />
Browse
</button>
</ButtonStyled>
</div>
</div>
</div>
<div
v-if="javaInstall && !overrideJavaInstall"
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
>
{{ javaInstall.path }}
</div>
</template>
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
</div>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaMemory) }}
</h2>
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2.5" />
<Checkbox
v-model="overrideMemorySettings"
:label="formatMessage(messages.customMemoryAllocation)"
class="mb-2"
/>
<Slider
id="max-memory"
v-model="memory.maximum"
@@ -159,28 +286,36 @@ const messages = defineMessages({
:snap-range="512"
unit="MB"
/>
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaArguments) }}
</h2>
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
<Checkbox
v-model="overrideJavaArgs"
:label="formatMessage(messages.customJavaArguments)"
class="my-2"
/>
<StyledInput
id="java-args"
v-model="javaArgs"
autocomplete="off"
:disabled="!overrideJavaArgs"
placeholder="Enter java arguments..."
:placeholder="formatMessage(messages.enterJavaArguments)"
wrapper-class="w-full"
/>
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
{{ formatMessage(messages.javaEnvironmentVariables) }}
</h2>
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2.5" />
<Checkbox
v-model="overrideEnvVars"
:label="formatMessage(messages.customEnvironmentVariables)"
class="mb-2"
/>
<StyledInput
id="env-vars"
v-model="envVars"
autocomplete="off"
:disabled="!overrideEnvVars"
placeholder="Enter environmental variables..."
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
wrapper-class="w-full"
/>
</div>
@@ -45,12 +45,19 @@ const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')
const instanceRef = computed(() => props.instance)
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function hide() {
tabbedModal.value?.hide()
}
provideInstanceSettings({
instance: instanceRef,
offline: props.offline,
isMinecraftServer,
onUnlinked: handleUnlinked,
closeModal: hide,
})
watch(
@@ -113,9 +120,6 @@ const tabs = computed<TabbedModalTab[]>(() => [
},
])
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function show(tabIndex?: number) {
if (props.instance.linked_data?.project_id) {
queryClient.prefetchQuery({
@@ -129,7 +133,7 @@ function show(tabIndex?: number) {
}
}
defineExpose({ show })
defineExpose({ show, hide })
</script>
<template>
<TabbedModal
@@ -2,16 +2,21 @@ import type { Labrinth } from '@modrinth/api-client'
import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets'
import type { CardAction } from '@modrinth/ui'
import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import type { ComputedRef, Ref } from 'vue'
import { onUnmounted, ref, shallowRef } from 'vue'
import type { Router } from 'vue-router'
import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { process_listener } from '@/helpers/events'
import { get_by_profile_path } from '@/helpers/process'
import { kill, list as listInstances } from '@/helpers/profile.js'
import type { GameInstance } from '@/helpers/types'
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
import { add_server_to_profile } from '@/helpers/worlds'
import { getServerAddress } from '@/store/install.js'
interface BrowseServerInstance {
@@ -65,14 +70,13 @@ const messages = defineMessages({
export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debugLog = useDebugLogger('BrowseServer')
const serverPings = shallowRef<Record<string, number | undefined>>({})
const serverPingCache = new Map<string, number | undefined>()
const pendingServerPings = new Map<string, Promise<number | undefined>>()
const runningServerProjects = ref<Record<string, string>>({})
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingCacheActive = true
let serverPingsActive = true
let unlistenProcesses: (() => void) | null = null
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
@@ -145,37 +149,26 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
})
const nextPings = { ...serverPings.value }
for (const { hit, address } of pingsToFetch) {
if (serverPingCache.has(address)) {
nextPings[hit.project_id] = serverPingCache.get(address)
const cachedStatus = getFreshCachedServerStatus(queryClient, address)
if (cachedStatus) {
nextPings[hit.project_id] = cachedStatus.ping
}
}
serverPings.value = nextPings
await Promise.all(
pingsToFetch.map(async ({ hit, address }) => {
if (serverPingCache.has(address)) return
if (getFreshCachedServerStatus(queryClient, address)) return
let pending = pendingServerPings.get(address)
if (!pending) {
pending = getServerLatency(address)
.then((latency) => {
if (serverPingCacheActive) serverPingCache.set(address, latency)
return latency
})
.catch((error) => {
console.error(`Failed to ping server ${address}:`, error)
if (serverPingCacheActive) serverPingCache.set(address, undefined)
return undefined
})
.finally(() => {
pendingServerPings.delete(address)
})
pendingServerPings.set(address, pending)
try {
const status = await fetchCachedServerStatus(queryClient, address)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: status.ping }
} catch (error) {
console.error(`Failed to ping server ${address}:`, error)
if (!serverPingsActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: undefined }
}
const latency = await pending
if (!serverPingCacheActive) return
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
}),
)
}
@@ -307,10 +300,8 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
.catch(options.handleError)
onUnmounted(() => {
serverPingCacheActive = false
serverPingsActive = false
unlistenProcesses?.()
serverPingCache.clear()
pendingServerPings.clear()
})
return {
@@ -0,0 +1,50 @@
import type { QueryClient } from '@tanstack/vue-query'
import {
get_server_status,
normalizeServerAddress,
type ProtocolVersion,
type ServerStatus,
} from '@/helpers/worlds'
export const SERVER_STATUS_CACHE_MS = 10 * 60 * 1000
function getProtocolVersionKey(protocolVersion: ProtocolVersion | null) {
if (!protocolVersion) return 'default'
return `${protocolVersion.version}:${protocolVersion.legacy ? 'legacy' : 'modern'}`
}
export function getServerStatusQueryKey(
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return [
'minecraft-server-status',
normalizeServerAddress(address) || address.trim().toLowerCase(),
getProtocolVersionKey(protocolVersion),
] as const
}
export function getFreshCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
const queryKey = getServerStatusQueryKey(address, protocolVersion)
const updatedAt = queryClient.getQueryState(queryKey)?.dataUpdatedAt ?? 0
if (!updatedAt || Date.now() - updatedAt >= SERVER_STATUS_CACHE_MS) return undefined
return queryClient.getQueryData<ServerStatus>(queryKey)
}
export async function fetchCachedServerStatus(
queryClient: QueryClient,
address: string,
protocolVersion: ProtocolVersion | null = null,
) {
return await queryClient.fetchQuery({
queryKey: getServerStatusQueryKey(address, protocolVersion),
queryFn: () => get_server_status(address, protocolVersion),
staleTime: SERVER_STATUS_CACHE_MS,
gcTime: SERVER_STATUS_CACHE_MS,
})
}
@@ -1,121 +0,0 @@
import { computed, onUnmounted, ref } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
interface ThemeStore {
toggleSidebar: boolean
$subscribe: (callback: () => void) => () => void
}
interface IntercomBubblePosition {
horizontalPadding: number
verticalPadding: number
}
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const INTERCOM_BUBBLE_WIDTH = 72
const INTERCOM_BUBBLE_RIGHT_VAR = '--app-support-launcher-right'
const INTERCOM_BUBBLE_BOTTOM_VAR = '--app-support-launcher-bottom'
export function useIntercomPositioning({
route,
themeStore,
}: {
route: RouteLocationNormalizedLoaded
themeStore: ThemeStore
}) {
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
onUnmounted(unsubscribeSidebarToggle)
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const defaultIntercomBubbleHorizontalPadding = computed(() =>
sidebarVisible.value
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
: INTERCOM_BUBBLE_DEFAULT_PADDING,
)
const intercomBubbleRequestedHorizontalPadding = ref<number | null>(null)
const intercomBubbleHorizontalPadding = computed(
() =>
intercomBubbleRequestedHorizontalPadding.value ??
defaultIntercomBubbleHorizontalPadding.value,
)
const intercomBubbleVerticalClearance = ref<number | null>(null)
const intercomBubblePosition = computed(() => ({
horizontalPadding: intercomBubbleHorizontalPadding.value,
verticalPadding: intercomBubbleVerticalClearance.value ?? INTERCOM_BUBBLE_DEFAULT_PADDING,
}))
const intercomBubbleHorizontalPaddingRequests = new Map<symbol, number>()
const intercomBubbleClearanceRequests = new Map<symbol, number>()
function requestIntercomBubbleHorizontalPadding(id: symbol, padding: number | null) {
if (padding === null) {
intercomBubbleHorizontalPaddingRequests.delete(id)
} else {
intercomBubbleHorizontalPaddingRequests.set(id, padding)
}
intercomBubbleRequestedHorizontalPadding.value =
intercomBubbleHorizontalPaddingRequests.size > 0
? Math.max(...intercomBubbleHorizontalPaddingRequests.values())
: null
}
function requestIntercomBubbleVerticalClearance(id: symbol, clearance: number | null) {
if (clearance === null) {
intercomBubbleClearanceRequests.delete(id)
} else {
intercomBubbleClearanceRequests.set(id, clearance)
}
intercomBubbleVerticalClearance.value =
intercomBubbleClearanceRequests.size > 0
? Math.max(...intercomBubbleClearanceRequests.values())
: null
}
function updateIntercomBubbleStyles({
horizontalPadding,
verticalPadding,
}: IntercomBubblePosition) {
if (typeof document === 'undefined') return
document.documentElement.style.setProperty(INTERCOM_BUBBLE_RIGHT_VAR, `${horizontalPadding}px`)
document.documentElement.style.setProperty(INTERCOM_BUBBLE_BOTTOM_VAR, `${verticalPadding}px`)
}
function clearIntercomBubbleStyles() {
if (typeof document === 'undefined') return
document.documentElement.style.removeProperty(INTERCOM_BUBBLE_RIGHT_VAR)
document.documentElement.style.removeProperty(INTERCOM_BUBBLE_BOTTOM_VAR)
}
return {
sidebarToggled,
forceSidebar,
sidebarVisible,
intercomBubblePosition,
updateIntercomBubbleStyles,
clearIntercomBubbleStyles,
pageContext: {
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: {
width: ref(INTERCOM_BUBBLE_WIDTH),
horizontalPadding: intercomBubbleHorizontalPadding,
requestHorizontalPadding: requestIntercomBubbleHorizontalPadding,
requestVerticalClearance: requestIntercomBubbleVerticalClearance,
},
},
}
}
@@ -0,0 +1,52 @@
import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { test_jre } from '@/helpers/jre.js'
export default function useJavaTest() {
const testingJava = ref(false)
const javaTestResult = ref<boolean | null>(null)
let testDebounceTimer: ReturnType<typeof setTimeout> | null = null
async function runJavaTest(path: string, version: number, track = true) {
if (testDebounceTimer) {
clearTimeout(testDebounceTimer)
testDebounceTimer = null
}
if (!path) {
javaTestResult.value = null
return
}
testingJava.value = true
try {
javaTestResult.value = await test_jre(path, version)
} catch {
javaTestResult.value = false
}
testingJava.value = false
if (track) {
trackEvent('JavaTest', { path, success: javaTestResult.value })
}
}
function testJavaInstallationDebounced(path: string, version: number, delay = 600) {
if (testDebounceTimer) clearTimeout(testDebounceTimer)
if (!path) {
javaTestResult.value = null
return
}
testDebounceTimer = setTimeout(() => runJavaTest(path, version, false), delay)
}
async function testJavaInstallation(path: string, version: number, track = false) {
await runJavaTest(path, version, track)
}
return {
testingJava,
javaTestResult,
testJavaInstallationDebounced,
testJavaInstallation,
}
}
@@ -0,0 +1,100 @@
import type {
ContentItem,
ContentModpackCardCategory,
ContentModpackCardProject,
ContentModpackCardVersion,
ContentOwner,
} from '@modrinth/ui'
import {
get_content_items,
get_linked_modpack_info,
type LinkedModpackInfo,
} from '@/helpers/profile'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour } from '@/helpers/types'
export type InstanceContentData = {
path: string
contentItems: ContentItem[] | null
modpack: InstanceContentModpackData | null
}
export type InstanceContentModpackData = {
project: ContentModpackCardProject
version: ContentModpackCardVersion
owner: ContentOwner | null
categories: ContentModpackCardCategory[]
hasUpdate: boolean
updateVersionId: string | null
}
export async function loadInstanceContentData(
path: string,
cacheBehaviour?: CacheBehaviour,
onError?: (error: Error) => unknown,
): Promise<InstanceContentData> {
const [contentItems, modpackInfo, allCategories] = await Promise.all([
get_content_items(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_linked_modpack_info(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
get_categories().catch((error) => handleLoadError(error, onError)),
])
return {
path,
contentItems: (contentItems as ContentItem[] | null | undefined) ?? null,
modpack: normalizeLinkedModpackInfo(
modpackInfo as LinkedModpackInfo | null | undefined,
allCategories as ContentModpackCardCategory[] | null | undefined,
),
}
}
function handleLoadError(error: unknown, onError?: (error: Error) => unknown) {
onError?.(error as Error)
return null
}
function normalizeLinkedModpackInfo(
modpackInfo: LinkedModpackInfo | null | undefined,
allCategories: ContentModpackCardCategory[] | null | undefined,
): InstanceContentModpackData | null {
if (!modpackInfo) return null
return {
project: {
...modpackInfo.project,
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
icon_url: modpackInfo.project.icon_url ?? undefined,
},
version: {
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
},
owner: modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null,
categories: resolveLinkedModpackCategories(modpackInfo, allCategories),
hasUpdate: modpackInfo.has_update,
updateVersionId: modpackInfo.update_version_id,
}
}
function resolveLinkedModpackCategories(
modpackInfo: LinkedModpackInfo,
allCategories: ContentModpackCardCategory[] | null | undefined,
) {
if (!allCategories || !modpackInfo.project.categories) return []
const seen = new Set<string>()
return allCategories.filter((category) => {
if (modpackInfo.project.categories.includes(category.name) && !seen.has(category.name)) {
seen.add(category.name)
return true
}
return false
})
}
+8 -1
View File
@@ -147,9 +147,16 @@ export async function get_mod_full_path(path: string, projectPath: string): Prom
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
}
export interface JavaVersion {
parsed_version: number
version: string
architecture: string
path: string
}
// Get optimal java version from profile
// Returns a java version
export async function get_optimal_jre_key(path: string): Promise<string | null> {
export async function get_optimal_jre_key(path: string): Promise<JavaVersion | null> {
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
}
@@ -0,0 +1,83 @@
import type { LocationQuery, LocationQueryRaw } from 'vue-router'
const MODRINTH_HOSTNAMES = new Set(['modrinth.com', 'www.modrinth.com'])
const SUPPORTED_PROJECT_TYPES = new Set([
'mod',
'modpack',
'resourcepack',
'datapack',
'plugin',
'shader',
'server',
'project',
])
export function parseModrinthLink(
href: string,
): { slug: string; pathSuffix: string; url: URL } | null {
let url: URL
try {
url = new URL(href)
} catch {
return null
}
if (!MODRINTH_HOSTNAMES.has(url.hostname.toLowerCase())) {
return null
}
const segments = url.pathname.split('/').filter((p) => p.length > 0)
if (segments.length < 2) {
return null
}
if (SUPPORTED_PROJECT_TYPES.has(segments[0].toLowerCase())) {
const slug = segments[1]
if (!slug) {
return null
}
const rest: string[] = segments.slice(2)
const pathSuffix = toValidAppSubpath(rest)
if (pathSuffix === null) {
return null
}
return { slug, pathSuffix, url }
} else {
return null
}
}
const SUPPORTED_SUBPATHS = ['versions', 'gallery']
function toValidAppSubpath(rest: string[]): string | null {
if (rest.length === 0) {
return ''
}
const subroute = rest[0].toLowerCase()
if (rest.length === 1 && SUPPORTED_SUBPATHS.includes(subroute)) {
return `/${subroute}`
}
if (rest.length === 2 && subroute === 'version') {
return `/version/${rest[1]}`
}
return null
}
export function mergeUrlQuery(routeQuery: LocationQuery, linkUrl: URL): LocationQueryRaw {
const newQuery: LocationQueryRaw = { ...routeQuery }
const keys = new Set<string>()
linkUrl.searchParams.forEach((_value, key) => {
keys.add(key)
})
for (const key of keys) {
const values = linkUrl.searchParams.getAll(key)
newQuery[key] = values.length === 1 ? values[0] : values
}
return newQuery
}
+4
View File
@@ -22,6 +22,10 @@ export async function removeEnqueuedUpdate() {
return await invoke('remove_enqueued_update')
}
export async function setRestartAfterPendingUpdate(should_restart) {
return await invoke('set_restart_after_pending_update', { shouldRestart: should_restart })
}
// One of 'Windows', 'Linux', 'MacOS'
export async function getOS() {
return await invoke('plugin:utils|get_os')
+21 -12
View File
@@ -1,16 +1,25 @@
{
"app.action-bar.downloading-java": {
"message": "جاز تنزيل إصدار جافا {version}"
},
"app.action-bar.downloads": {
"message": "التنزيلات"
},
"app.action-bar.offline": {
"message": "غير متصل"
},
"app.appearance-settings.default-landing-page.home": {
"message": "صفحة الرئيسية"
},
"app.appearance-settings.default-landing-page.library": {
"message": "مكتبة"
},
"app.auth-servers.unreachable.body": {
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
},
"app.auth-servers.unreachable.header": {
"message": "تعذر الوصول إلى خوادم المصادقة"
},
"app.browse.add-server-to-instance": {
"message": "أضف الخادم للنموذج"
},
"app.browse.add-servers-to-instance": {
"message": "أضف الخوادم لنموذجك"
},
"app.browse.add-to-instance": {
"message": "أضف للنموذج"
},
@@ -29,9 +38,6 @@
"app.browse.discover-servers": {
"message": "استكشف خوادم"
},
"app.browse.install-content-to-instance": {
"message": "تثبيت محتوى لنموذجك"
},
"app.browse.server.installing": {
"message": "جاري التثبيت"
},
@@ -50,9 +56,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "إسم حزمة التعديل"
},
"app.export-modal.select-files-label": {
"message": "حدد الملفات والمجلدات المراد تضمينها في الحزمة"
},
"app.export-modal.version-number-label": {
"message": "رقم الإصدار"
},
@@ -572,6 +575,12 @@
"instance.worlds.world_in_use": {
"message": "العالم قيد الاستخدام"
},
"minecraft-account.add-account": {
"message": "إضافة حساب"
},
"minecraft-account.select-account": {
"message": "اختيار الحساب"
},
"search.filter.locked.instance": {
"message": "مقدَّم من النسخة"
},
+240 -6
View File
@@ -1,15 +1,114 @@
{
"app.action-bar.downloading-java": {
"message": "Stahování Javy {version}"
},
"app.action-bar.downloads": {
"message": "Stažené Soubory"
},
"app.action-bar.hide-more-running-instances": {
"message": "Skrýt více spuštěných instancí"
},
"app.action-bar.make-primary-instance": {
"message": "Použít jako primární instanci"
},
"app.action-bar.no-instances-running": {
"message": "Žádné spuštěné instance"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primární instance"
},
"app.action-bar.show-more-running-instances": {
"message": "Zobrazit další spuštěné instance"
},
"app.action-bar.stop-instance": {
"message": "Ukončit instanci"
},
"app.action-bar.view-active-downloads": {
"message": "Zobrazit probíhající stahování"
},
"app.action-bar.view-instance": {
"message": "Zobrazit instanci"
},
"app.action-bar.view-logs": {
"message": "Zobrazit záznamy"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Umožňuje pokročilé vykreslování, jako jsou efekty rozostření, které mohou způsobit problémy s výkonem bez hardwarového vykreslení."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Pokročilé vykreslování"
},
"app.appearance-settings.color-theme.description": {
"message": "Vyberte si váš preferovaný motiv pro aplikaci Modrinth."
},
"app.appearance-settings.color-theme.title": {
"message": "Barevný motiv"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Změňte stránku, na které se spouštěč otevře."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Domov"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Knihovna"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Výchozí úvodní stránka"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Zakáže zobrazení jmenovky nad vaší postavou na stránce skinů."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Skrýt jmenovku"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "V sekci „Vrátit se do hry“ na úvodní stránce jsou uvedeny nejnovější světy."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Skočit zpět do světů"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizovat spouštěč při spuštění procesu Minecraftu."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimalizovat spouštěč"
},
"app.appearance-settings.native-decorations.description": {
"message": "Použít systémový rámeček okna (vyžaduje restart aplikace)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Nativní dekorace"
},
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Umožňí přepínat zobrazení postranního panelu."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Přepnout postranní panel"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Pokud se pokusíte nainstalovat soubor Modrinth Pack (.mrpack), který není hostován na Modrinth, zajistíme, abyste si před instalací uvědomili související rizika."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Upozorni mě před instalací neznámých modpacků"
},
"app.auth-servers.unreachable.body": {
"message": "Autořizační servery Minecraftu aktuálně nejsou k dispozici. Zkontrolujte si své připojení k internetu a zkuste to znovu později."
},
"app.auth-servers.unreachable.header": {
"message": "Připojení k autorizačním serverům se nezdařilo"
},
"app.browse.add-server-to-instance": {
"app.browse.add-servers-to-instance": {
"message": "Přidat server do instance"
},
"app.browse.add-servers-to-instance": {
"message": "Přidat servery do instance"
"app.browse.add-to-an-instance": {
"message": "Přidat do instalace"
},
"app.browse.add-to-instance": {
"message": "Přidat do instalace"
@@ -23,14 +122,32 @@
"app.browse.already-added": {
"message": "Už přidáno"
},
"app.browse.back-to-instance": {
"message": "Zpět k instanci"
},
"app.browse.discover-content": {
"message": "Prozkoumat obsah"
},
"app.browse.discover-servers": {
"message": "Prozkoumat servery"
},
"app.browse.install-content-to-instance": {
"message": "Nainstalovat obsah do instnce"
"app.browse.hide-added-servers": {
"message": "Skrýt již přidané servery"
},
"app.browse.project-type.modpacks": {
"message": "Balíčky Modifikací"
},
"app.browse.server-instance-content-warning": {
"message": "Přidání obsahu může narušit kompatibilitu při připojení k serveru. Veškerý přidaný obsah se navíc ztratí při aktualizaci obsahu instance serveru."
},
"app.browse.server.installing": {
"message": "Instalování"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Probíhá instalace modpacku..."
},
"app.export-modal.description-placeholder": {
"message": "Přidej popis modpacku..."
@@ -41,6 +158,9 @@
"app.export-modal.header": {
"message": "Exportovat modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Zahrnout \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Jméno Modpacku"
},
@@ -48,7 +168,7 @@
"message": "Jméno modpacku"
},
"app.export-modal.select-files-label": {
"message": "Vybrat soubory a složky co zahrnout do modpacku"
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
},
"app.export-modal.version-number-label": {
"message": "Číslo verze"
@@ -131,6 +251,9 @@
"app.instance.worlds.remove-server-description": {
"message": "„{name}“ bude odstraněn z tvého seznamu, včetně hry, a nebude žádný způsob, jak ho obnovit."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) bude odstraněn z vašeho seznamu, a to i ve hře, a nebude možné jej obnovit."
},
"app.instance.worlds.remove-server-title": {
"message": "Opravdu chceš odstranit {name}?"
},
@@ -140,6 +263,9 @@
"app.instance.worlds.this-server": {
"message": "tento server"
},
"app.modal.install-to-play.content-required": {
"message": "Požadovaný obsah"
},
"app.modal.install-to-play.header": {
"message": "Nainstaluj ke hraní"
},
@@ -149,12 +275,21 @@
"app.modal.install-to-play.mod-count": {
"message": "{count, plural,one {#mód} other {#módy}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Povinný modpack"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "Tento server k hraní vyžaduje módy. Klikni na instalovat pro získání potřebných módů z Modrinth."
},
"app.modal.install-to-play.shared-instance": {
"message": "Sdílená instance"
},
"app.modal.install-to-play.shared-server-instance": {
"message": "Sdílená serverová instance"
},
"app.modal.install-to-play.view-contents": {
"message": "Zobrazit obsah"
},
"app.modal.update-to-play.header": {
"message": "Updatuj ke hraní"
},
@@ -164,6 +299,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "Je nutná aktualizace ke hraní {name}. Prosím updatuj na nejnovější verzi k spuštění hry."
},
"app.project.install-button.already-installed": {
"message": "Tento projekt je již nainstalován"
},
"app.project.install-context.install-content-to-instance": {
"message": "Nainstalovat obsah do instnce"
},
"app.settings.developer-mode-enabled": {
"message": "Vývojářský režim povolen."
},
@@ -188,9 +329,33 @@
"app.settings.tabs.resource-management": {
"message": "Správa zdrojů"
},
"app.update-popup.body": {
"message": "Aplikace Modrinth v{version} je připravena k instalaci! Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-popup.body.download-complete": {
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
},
"app.update-popup.body.linux": {
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
},
"app.update-popup.body.metered": {
"message": "Aplikace Modrinth v{version} je nyní k dispozici! Protože jste v měřené síti, nebyla stažena automaticky."
},
"app.update-popup.changelog": {
"message": "Seznam změn"
},
"app.update-popup.download": {
"message": "Stáhnout ({size})"
},
"app.update-popup.download-complete": {
"message": "Stahování bylo dokončeno"
},
"app.update-popup.reload": {
"message": "Načíst znovu"
},
"app.update-popup.title": {
"message": "Je k dispozici aktualizace"
},
"app.update.complete-toast.text": {
"message": "Kliknutím sem zobrazíte seznam změn."
},
@@ -206,6 +371,24 @@
"app.update.reload-to-update": {
"message": "Restartovat aplikaci pro nainstalování aktualizace"
},
"app.world.server-modal.placeholder-address": {
"message": "priklad.modrinth.gg"
},
"app.world.server-modal.select-an-option": {
"message": "Vyberte možnost"
},
"app.world.world-item.incompatible-version": {
"message": "Nekompatibilní verze {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Ještě nehráno"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Přidat přítele"
},
@@ -305,6 +488,12 @@
"instance.edit-world.title": {
"message": "Upravit svět"
},
"instance.files.adding-files": {
"message": "Přidávání souborů ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Uložit jako..."
},
"instance.server-modal.address": {
"message": "Adresa"
},
@@ -485,6 +674,9 @@
"instance.worlds.incompatible_server": {
"message": "Server je nekompatibilní"
},
"instance.worlds.linked_server": {
"message": "Spravováno projektem serveru"
},
"instance.worlds.no_contact": {
"message": "Server se nepodařilo spojit"
},
@@ -503,6 +695,24 @@
"instance.worlds.world_in_use": {
"message": "Svět je použit"
},
"minecraft-account.add-account": {
"message": "Přidat účet"
},
"minecraft-account.label": {
"message": "Účet Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Nejste přihlášeni"
},
"minecraft-account.remove-account": {
"message": "Odstranit účet"
},
"minecraft-account.select-account": {
"message": "Vyberte účet"
},
"minecraft-account.sign-in": {
"message": "Přihlásit se do Minecraftu"
},
"search.filter.locked.instance": {
"message": "Poskytováno instancí"
},
@@ -515,6 +725,9 @@
"search.filter.locked.instance.sync": {
"message": "Synchronizováno z instancí"
},
"search.filter.locked.server": {
"message": "Poskytováno serverem"
},
"search.filter.locked.server-environment.title": {
"message": "Jen klientské mody mohou být přidány na server"
},
@@ -523,5 +736,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader zprostředkovává server"
},
"unknown-pack-warning-modal.body": {
"message": "Soubor je zkontrolován pouze v případě, že je nahrán na Modrinth, a to bez ohledu na jeho formát (včetně formátu .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Toto varování již nezobrazovat"
},
"unknown-pack-warning-modal.header": {
"message": "Potvrdit instalaci"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Přesto nainstalovat"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Malware se často šíří prostřednictvím souborů s modpacky, které se sdílejí na platformách jako Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Tento soubor jsme na Modrinthu nenašli. Důrazně doporučujeme instalovat pouze soubory z důvěryhodných zdrojů."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Upozornění na neznámý soubor"
}
}
+84 -3
View File
@@ -1,4 +1,88 @@
{
"app.action-bar.downloading-java": {
"message": "Downloader Java {version}"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Gem flere kørende instances"
},
"app.action-bar.make-primary-instance": {
"message": "Gør til primære instance"
},
"app.action-bar.no-instances-running": {
"message": "Ingen instances kørende"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primære instance"
},
"app.action-bar.show-more-running-instances": {
"message": "Vis flere kørende instances"
},
"app.action-bar.stop-instance": {
"message": "Stop instance"
},
"app.action-bar.view-active-downloads": {
"message": "Vis aktive downloads"
},
"app.action-bar.view-instance": {
"message": "Vis instance"
},
"app.action-bar.view-logs": {
"message": "Vis logs"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Avanceret rendering"
},
"app.appearance-settings.color-theme.description": {
"message": "Vælg dit foretrukne farvetema til Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Farvetema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Skift hvilken side launcheren åbner i."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Hjem"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standard startside"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Slår nametagget over din spiller på skins siden fra."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Gem nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Inkluderer de sidste verdener i afsnittet \"Hop tilbage\" på startsiden."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Hop tilbage ind i verdener"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimer launcheren når Minecraft starter."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimer launcher"
},
"app.appearance-settings.select-option": {
"message": "Vælg en mulighed"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Hvis du prøver at installere en Modrinth Pack fil (.mrpack) som ikke er hosted på Modrinth, så skal vi nok sikre os du forstår riskoen før du installere den."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Advar mig før jeg installere en ukendt modpacks"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraft authentication servere kan måske være nede lige nu. Tjek din internet forbindelse og prøv igen senere."
},
@@ -35,9 +119,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modpack navn"
},
"app.export-modal.select-files-label": {
"message": "Vælg filer og mapper til at inkludere i pakken"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
+21 -15
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Authentifizierungsserver sind nicht erreichbar"
},
"app.browse.add-server-to-instance": {
"app.browse.add-servers-to-instance": {
"message": "Server zu Instanz hinzufügen"
},
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zu Instanz hinzufügen"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Bereits hinzugefügt"
},
"app.browse.back-to-instance": {
"message": "Zu Instanz zurückgehen"
},
"app.browse.discover-content": {
"message": "Inhalte entdecken"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "Bereits hinzugefügte Server ausblenden"
},
"app.browse.hide-installed-content": {
"message": "Bereits installierte Inhalte ausblenden"
},
"app.browse.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Installieren"
},
"app.browse.server.installed": {
"message": "Installiert"
"app.browse.server-instance-content-warning": {
"message": "Das hinzufügen von Inhalten kann Kompatibilitätsprobleme beim beitreten eines Servers verursachen. Sämtliche hinzugefügte Inhalten gehen ausserdem beim aktualisieren der Server Instanz-Inhalte verloren."
},
"app.browse.server.installing": {
"message": "Wird installiert"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Modpack exportieren"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\" einschliessen?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpaketname"
},
@@ -171,7 +168,7 @@
"message": "Modpaketname"
},
"app.export-modal.select-files-label": {
"message": "Wähle Dateien und Ordner zum hinzufügen im Paket aus"
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Eine aktualisierung zum spielen von {name} ist benötigt. Bitte aktualisiere auf die neuste Version um das Spiel zu starten."
},
"app.project.install-button.already-installed": {
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Entdecken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.settings.developer-mode-enabled": {
"message": "Entwicklermodus aktiviert."
},
+25 -19
View File
@@ -12,7 +12,7 @@
"message": "Zur primären Instanz machen"
},
"app.action-bar.no-instances-running": {
"message": "Keine Instanzen laufen"
"message": "Keine aktiven Instanzen"
},
"app.action-bar.offline": {
"message": "Offline"
@@ -48,7 +48,7 @@
"message": "Farbschema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
"message": "Ändere die Kategorie, die beim Öffnen des Launchers angezeigt wird."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Start"
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Authentifizierungsserver sind nicht erreichbar"
},
"app.browse.add-server-to-instance": {
"message": "Server zur Instanz hinzufügen"
},
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
"message": "Server zu Instanz hinzufügen"
},
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zur Instanz hinzufügen"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Bereits hinzugefügt"
},
"app.browse.back-to-instance": {
"message": "Zurück zur Instanz"
},
"app.browse.discover-content": {
"message": "Inhalte entdecken"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "Bereits hinzugefügte Server ausblenden"
},
"app.browse.hide-installed-content": {
"message": "Bereits installierte Inhalte ausblenden"
},
"app.browse.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Installieren"
},
"app.browse.server.installed": {
"message": "Installiert"
"app.browse.server-instance-content-warning": {
"message": "Das Hinzufügen von Inhalten kann die Kompatibilität beim Beitreten zum Server beeinträchtigen. Außerdem gehen alle hinzugefügten Inhalte verloren, wenn du die Inhalte der Serverinstanz aktualisierst."
},
"app.browse.server.installing": {
"message": "Wird installiert"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Modpack exportieren"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\" einschließen?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpackname"
},
@@ -171,7 +168,7 @@
"message": "Modpackname"
},
"app.export-modal.select-files-label": {
"message": "Wähle Dateien und Ordner aus, die in das Paket sollen"
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Zum Spielen von {name} ist eine Aktualisierung erforderlich. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
},
"app.project.install-button.already-installed": {
"message": "Dieses Projekt ist bereits installiert"
},
"app.project.install-context.back-to-browse": {
"message": "Zurück zum Entdecken"
},
"app.project.install-context.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.settings.developer-mode-enabled": {
"message": "Entwicklermodus aktiviert."
},
@@ -519,7 +525,7 @@
"message": "Wird gelöscht..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Kopieren"
"message": "Duplizieren"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Kann während der Installation nicht dupliziert werden."
+25 -1
View File
@@ -143,6 +143,9 @@
"app.browse.server.installing": {
"message": "Installing"
},
"app.browse.server.world-fallback-name": {
"message": "Instance"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
@@ -303,7 +306,7 @@
"message": "This project is already installed"
},
"app.project.install-context.back-to-browse": {
"message": "Back to browse"
"message": "Back to discover"
},
"app.project.install-context.install-content-to-instance": {
"message": "Install content to instance"
@@ -611,6 +614,24 @@
"instance.settings.tabs.java": {
"message": "Java and memory"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Custom environment variables"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Custom Java arguments"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Custom Java installation"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Custom memory allocation"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Enter environmental variables..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Enter Java arguments..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Environment variables"
},
@@ -626,6 +647,9 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memory allocated"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Window"
},
+68 -44
View File
@@ -12,43 +12,43 @@
"message": "Hacer instancia principal"
},
"app.action-bar.no-instances-running": {
"message": "Ninguna instancia en ejecución"
"message": "No hay instancias en ejecución"
},
"app.action-bar.offline": {
"message": "Sin conexión"
"message": "Desconectado"
},
"app.action-bar.primary-instance": {
"message": "Instancia principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mas instancias en ejecución"
"message": "Mostrar más instancias en ejecución"
},
"app.action-bar.stop-instance": {
"message": "Finalizar instancia"
"message": "Detener instancia"
},
"app.action-bar.view-active-downloads": {
"message": "Mostrar descargas activas"
"message": "Ver descargas en curso"
},
"app.action-bar.view-instance": {
"message": "Mostrar instancia"
"message": "Ver instancia"
},
"app.action-bar.view-logs": {
"message": "Mostrar registros"
"message": "Ver registros"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Habilita el renderizado avanzado, como los efectos de desenfoque, que pueden causar problemas de rendimiento sin renderizado acelerado por hardware."
"message": "Activa el renderizado avanzado, como los efectos de desenfoque, que pueden afectar el rendimiento sin aceleración por hardware."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderizado avanzado"
},
"app.appearance-settings.color-theme.description": {
"message": "Seleccione su tema de color preferido para Modrinth en este dispositivo."
"message": "Selecciona el tema de color que prefieras para la Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema de color"
"message": "Color de la interfaz"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Cambia la página en la que el launcher se abre en."
"message": "Cambia la página en la que se abre el launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Inicio"
@@ -57,28 +57,28 @@
"message": "Librería"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Página predeterminada"
"message": "Página de inicio predeterminada"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Desactiva la etiqueta arriba de tu personaje en la página de skins."
"message": "Desactiva la etiqueta de nombre arriba de tu personaje en la página de skins."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Ocultar etiqueta"
"message": "Ocultar etiqueta de nombre"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Incluye los mundos más recientes en la sección \"Vuelve a jugar tus mundos\" en la página de inicio."
"message": "Incluye los mundos recientes en la sección \"Volver a jugar\" de la página de inicio."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Vuelve a jugar tus mundos"
"message": "Volver a jugar mundos"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimiza el launcher cuando un proceso de Minecraft empieza."
"message": "Minimiza el launcher al iniciar un proceso de Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimiza el launcher"
"message": "Minimizar launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa los sistemas de frame de Windows (se requiere resetear la aplicación)."
"message": "Usar el borde de ventana del sistema (requiere reiniciar la aplicación)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decoraciones nativas"
@@ -87,16 +87,16 @@
"message": "Selecciona una opción"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Activa la posibilidad de alternar la barra lateral."
"message": "Permite mostrar u ocultar la barra lateral."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Alternar barra lateral"
"message": "Mostrar u ocultar la barra lateral"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, te advertiremos de los riesgos antes de instalarlo."
"message": "Si intentas instalar un paquete de Modrinth (.mrpack) que no está alojado en Modrinth, te explicaremos los riesgos antes de instalarlo."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Adviérteme antes de instalarme modpacks desconocidos"
"message": "Advertir antes de instalar modpacks desconocidos"
},
"app.auth-servers.unreachable.body": {
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede acceder a los servidores de autenticación"
},
"app.browse.add-server-to-instance": {
"message": "Añadir servidor a instancia"
},
"app.browse.add-servers-to-instance": {
"message": "Añade servidores a tu instancia"
"message": "Añadiendo servidor a instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a instancia"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Ya se ha añadido"
},
"app.browse.back-to-instance": {
"message": "Volver a instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "Ocultar servidores ya añadidos"
},
"app.browse.hide-installed-content": {
"message": "Ocultar contenido instalado"
},
"app.browse.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Instalar"
},
"app.browse.server.installed": {
"message": "Instalado"
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede romper la compatibilidad cuando te unes al servidor. Cualquier contenido añadido tambien se perderá cuando actualizes el contenido en la instancia del servidor."
},
"app.browse.server.installing": {
"message": "Instalando"
@@ -153,7 +147,7 @@
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Instalando el modpack..."
"message": "Instalando modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Introduce la descripción del modpack..."
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Exportar modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "¿Incluir \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nombre del modpack"
},
@@ -171,7 +168,7 @@
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Selecciona archivos y carpetas para incluirlos en el pack"
"message": "Configura que archivos incluir en esta exportación"
},
"app.export-modal.version-number-label": {
"message": "Número de la versión"
@@ -270,7 +267,7 @@
"message": "Contenido requerido"
},
"app.modal.install-to-play.header": {
"message": "Instala para jugar"
"message": "Instalar para jugar"
},
"app.modal.install-to-play.install-button": {
"message": "Instalar"
@@ -294,7 +291,7 @@
"message": "Ver contenidos"
},
"app.modal.update-to-play.header": {
"message": "Actualiza para jugar"
"message": "Actualizar para jugar"
},
"app.modal.update-to-play.update-required": {
"message": "Actualización requerida"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Se requiere una actualización para jugar {name}. Por favor, actualiza a la versión más reciente para iniciar el juego."
},
"app.project.install-button.already-installed": {
"message": "Este proyecto está actualmente instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Volver al explorador"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
},
"app.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
@@ -692,6 +698,24 @@
"instance.worlds.world_in_use": {
"message": "El mundo ya está en uso"
},
"minecraft-account.add-account": {
"message": "Añadir cuenta"
},
"minecraft-account.label": {
"message": "Cuenta de Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "No has inciado sesión"
},
"minecraft-account.remove-account": {
"message": "Eliminar cuenta"
},
"minecraft-account.select-account": {
"message": "Seleccionar cuenta"
},
"minecraft-account.sign-in": {
"message": "Inicia sesión en Minecraft"
},
"search.filter.locked.instance": {
"message": "Proporcionado por la instancia"
},
@@ -720,7 +744,7 @@
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "No mostrar esta advertencia otra vez"
"message": "No volver a mostrar esta advertencia"
},
"unknown-pack-warning-modal.header": {
"message": "Confirmar instalación"
@@ -729,7 +753,7 @@
"message": "Instalar de todos modos"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "El malware es normalmente distribuido por archivos de modpack que son normalmente compartidas en aplicaciones como Discord."
"message": "El malware a menudo es distribuido mediante modpacks compartidos en aplicaciones como Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "No pudimos encontrar este archivo en Modrinth. Recomendamos solo instalar archivos de sitios de confianza."
@@ -104,12 +104,6 @@
"app.auth-servers.unreachable.header": {
"message": "No se puede conectar con los servidores de autenticación"
},
"app.browse.add-server-to-instance": {
"message": "Añadir servidor a la instancia"
},
"app.browse.add-servers-to-instance": {
"message": "Añadir servidor a tu instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a la instancia"
},
@@ -131,21 +125,9 @@
"app.browse.hide-added-servers": {
"message": "Ocultar servidores ya añadidos"
},
"app.browse.hide-installed-content": {
"message": "Ocultar contenido instalado"
},
"app.browse.install-content-to-instance": {
"message": "Instalar contenido a una instancia"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Instalar"
},
"app.browse.server.installed": {
"message": "Instalado"
},
"app.browse.server.installing": {
"message": "Instalando"
},
@@ -170,9 +152,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Seleccione archivos y carpetas para incluir en el paquete"
},
"app.export-modal.version-number-label": {
"message": "Número de versión"
},
@@ -5,12 +5,6 @@
"app.auth-servers.unreachable.header": {
"message": "Todennuspalvelimiin ei saada yhteyttä"
},
"app.browse.add-server-to-instance": {
"message": "Lisää palvelin instanssiin"
},
"app.browse.add-servers-to-instance": {
"message": "Lisää palvelimia instanssiisi"
},
"app.browse.add-to-instance": {
"message": "Lisää instanssiin"
},
@@ -29,9 +23,6 @@
"app.browse.discover-servers": {
"message": "Löydä palvelimia"
},
"app.browse.install-content-to-instance": {
"message": "Lataa sisältöä instanssiin"
},
"app.export-modal.description-placeholder": {
"message": "Lisää modipaketin kuvaus..."
},
@@ -47,9 +38,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modipaketin nimi"
},
"app.export-modal.select-files-label": {
"message": "Valitse tiedostot ja kansiot pakettiin"
},
"app.export-modal.version-number-label": {
"message": "Versio numero"
},
+189 -9
View File
@@ -1,16 +1,109 @@
{
"app.action-bar.downloading-java": {
"message": "Dina-download ang Java {version}"
},
"app.action-bar.downloads": {
"message": "Mga downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Itago ang mas marami pang tumatakbong instansiya"
},
"app.action-bar.make-primary-instance": {
"message": "Gumawa ng pangunahing instansiya"
},
"app.action-bar.no-instances-running": {
"message": "Walang instansiya ang tumatakbo"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Pangunahing instansiya"
},
"app.action-bar.show-more-running-instances": {
"message": "Ipakita ang mas marami pang tumatakbong instansiya"
},
"app.action-bar.stop-instance": {
"message": "Itigil ang instansiya"
},
"app.action-bar.view-active-downloads": {
"message": "Tignan ang mga active downloads"
},
"app.action-bar.view-instance": {
"message": "Tignan ang instansiya"
},
"app.action-bar.view-logs": {
"message": "Tignan ang logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Pagpapagana ng masalimuot na pagrerender katulad ng paglalabo na maaaring magdudulot ng mga isyu sa performance kapag walang hardware-acceleration."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Masalimuot na pagrerender"
},
"app.appearance-settings.color-theme.description": {
"message": "Piliin ang iyong mas gustong kulay para sa Modrinth app."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema ng kulay"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Pagpapalit ng pahina na bubuksan ng launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Tahanan"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Librarya"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Panimulang pahina ng paglapag"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Pagtatago ng nametag na nasa itaas ng iyong manlalaro sa loob ng pahina ng mga skin."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Itago ang nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Pagsasali ng mga kamakailang mundo sa seksiyong \"Jump back in\" sa pahina ng Tahanan."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Bumalik sa mga mundo"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Liitan ang launcher kung nag-start ang isang proseso ng Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Liitin ang launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gamitin ang system window frame (kailangan i-restart ang app)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Native decorations"
},
"app.appearance-settings.select-option": {
"message": "Pumili ng opsiyon"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Pagpapagana na mata-toggle ang sidebar."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "I-toggle ang sidebar"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Kung susubukan mong mag-install ng Modrinth Pack file (.mrpack) na hindi naka-host sa Modrinth, titiyakin namin na nakaaalam ka sa mga panganib bago ito ma-install."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Pakipaalala bago ko ma-install ang mga di-kilalang modpack"
},
"app.auth-servers.unreachable.body": {
"message": "Maaaring hindi maaabot ang mga authentication server ng Minecraft sa ngayon. Tingnan mo ang iyong internet connection at muling subukan mamaya."
},
"app.auth-servers.unreachable.header": {
"message": "Hindi maabot ang mga authentication server"
},
"app.browse.add-server-to-instance": {
"message": "Idagdag ang server sa instansiya"
},
"app.browse.add-servers-to-instance": {
"message": "Idagdag ang mga server sa iyong instansiya"
},
"app.browse.add-to-instance": {
"message": "Idagdag sa instansiya"
},
@@ -29,8 +122,20 @@
"app.browse.discover-servers": {
"message": "Tumuklas ng mga server"
},
"app.browse.install-content-to-instance": {
"message": "I-install ang kontento sa instansiya"
"app.browse.hide-added-servers": {
"message": "Itago ang mga nailagay na mga servers"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.installing": {
"message": "Ini-install"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Ini-install ang modpack..."
},
"app.export-modal.description-placeholder": {
"message": "Ilagay ang paglalarawan ng modpack..."
@@ -41,6 +146,9 @@
"app.export-modal.header": {
"message": "Iluwas ang modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Salihin ang \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Pangalan ng Modpack"
},
@@ -48,7 +156,7 @@
"message": "Pangalan ng modpack"
},
"app.export-modal.select-files-label": {
"message": "Pumili ng mga talaksan at folder na isasali sa pack"
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
},
"app.export-modal.version-number-label": {
"message": "Numero ng bersiyon"
@@ -101,6 +209,15 @@
"app.instance.worlds.add-server": {
"message": "Magdagdag ng server"
},
"app.instance.worlds.browse-servers": {
"message": "Mag-browse ng servers"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' ay **permanenteng mabubura**, at walang paraan upang maibalik muli."
},
"app.instance.worlds.delete-world-title": {
"message": "Sigurado ka bang gusto mong permanenteng burahin ang world na ito?"
},
"app.instance.worlds.filter-modded": {
"message": "Modded"
},
@@ -113,6 +230,18 @@
"app.instance.worlds.filter-vanilla": {
"message": "Vanilla"
},
"app.instance.worlds.no-worlds-description": {
"message": "Maglagay ng server o mag-browse upang mag-simula"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Walang servers o worlds ang nalalagay"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
},
"app.instance.worlds.remove-server-title": {
"message": "Sigurado ka bang gusto mong tanggalin ang {name}?"
},
@@ -233,9 +362,15 @@
"app.world.world-item.incompatible-version": {
"message": "Di-magkatugmang bersiyong {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Hindi pa nalalaro"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Magdagdag ng kaibigan"
},
@@ -335,6 +470,12 @@
"instance.edit-world.title": {
"message": "Baguhin ang mundo"
},
"instance.files.adding-files": {
"message": "Idinadagdag ang mga file ({completed}/{total})"
},
"instance.files.save-as": {
"message": "I-save bilang..."
},
"instance.server-modal.address": {
"message": "Adres"
},
@@ -536,6 +677,24 @@
"instance.worlds.world_in_use": {
"message": "Ginagamit ang mundo"
},
"minecraft-account.add-account": {
"message": "Maglagay ng account"
},
"minecraft-account.label": {
"message": "Minecraft account"
},
"minecraft-account.not-signed-in": {
"message": "Hindi pa naka sign in"
},
"minecraft-account.remove-account": {
"message": "Itanggal ang account"
},
"minecraft-account.select-account": {
"message": "Pumili ng account"
},
"minecraft-account.sign-in": {
"message": "Mag sign in kay Minecraft"
},
"search.filter.locked.instance": {
"message": "Sagot na ng instansiya"
},
@@ -559,5 +718,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Ang loader ay handog na ng server"
},
"unknown-pack-warning-modal.body": {
"message": "Ang file ay nasusuri lamang kapag ito ay na-upload sa Modrinth, walang pili sa file format nito (kabilang na ang .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Huwag ipakita ang babalang ito ulit"
},
"unknown-pack-warning-modal.header": {
"message": "Kumpirmahin ang installation"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "I-install parin"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Ang malware ay madalas naidadala sa mga modpack files sa pamamagitan ng pag-bigay ng mga ito sa mga platforms kagaya ng Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Hindi namin mahanap ang file na ito sa Modrinth. Mahalagang mag-install ka lamang ng files galing sa mga mapagkakatiwalang sources."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Hindi kilalang file"
}
}
+25 -19
View File
@@ -1,6 +1,6 @@
{
"app.action-bar.downloading-java": {
"message": "Java version {version} en cours de téléchargement"
"message": "Téléchargement de Java {version} en cours"
},
"app.action-bar.downloads": {
"message": "Téléchargements"
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Impossible de contacter les serveurs d'authentification"
},
"app.browse.add-server-to-instance": {
"app.browse.add-servers-to-instance": {
"message": "Ajouter le serveur à l'instance"
},
"app.browse.add-servers-to-instance": {
"message": "Ajouter des serveurs à votre instance"
"app.browse.add-to-an-instance": {
"message": "Ajouter à une instance"
},
"app.browse.add-to-instance": {
"message": "Ajouter à l'instance"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Déjà ajouté"
},
"app.browse.back-to-instance": {
"message": "Retour à l'instance"
},
"app.browse.discover-content": {
"message": "Découvrir du contenu"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "Masquer les serveurs déjà ajoutés"
},
"app.browse.hide-installed-content": {
"message": "Masquer le contenu déjà ajouté"
},
"app.browse.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server.install": {
"message": "Installer"
},
"app.browse.server.installed": {
"message": "Installé"
"app.browse.server-instance-content-warning": {
"message": "Ajouter du contenu peut briser la comptabilité en rejoignant le serveur. Tout contenu en plus sera également perdu lorsque vous mettrez à jour le contenu de l'instance du serveur."
},
"app.browse.server.installing": {
"message": "Installation en cours"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Exporter le modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Inclure « {file} » ?"
},
"app.export-modal.modpack-name-label": {
"message": "Nom du modpack"
},
@@ -171,7 +168,7 @@
"message": "Nom du modpack"
},
"app.export-modal.select-files-label": {
"message": "Sélectionnez des fichiers et des dossiers à inclure dans le pack"
"message": "Configurez quels fichiers sont inclus dans cette exportation"
},
"app.export-modal.version-number-label": {
"message": "Numéro de version"
@@ -192,7 +189,7 @@
"message": "Supprimer l'instance"
},
"app.instance.modpack-already-installed.body": {
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr.e de vouloir le dupliquer ?"
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr·e de vouloir le dupliquer ?"
},
"app.instance.modpack-already-installed.create": {
"message": "Créer"
@@ -231,7 +228,7 @@
"message": "« {name} » sera supprimé **pour toujours**, et il sera impossible de le récupérer."
},
"app.instance.worlds.delete-world-title": {
"message": "Êtes-vous sûr.e de vouloir supprimer ce monde pour toujours ?"
"message": "Êtes-vous sûr·e de vouloir supprimer ce monde pour toujours ?"
},
"app.instance.worlds.filter-modded": {
"message": "Moddé"
@@ -258,7 +255,7 @@
"message": "« {name} » ({address}) sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
},
"app.instance.worlds.remove-server-title": {
"message": "Êtes-vous sûr.e de vouloir retirer {name} ?"
"message": "Êtes-vous sûr·e de vouloir retirer {name} ?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Rechercher {count} mondes..."
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Une mise à jour est requise pour jouer à {name}. Veuillez mettre à jour à la dernière version pour lancer le jeu."
},
"app.project.install-button.already-installed": {
"message": "Ce projet a déjà été installé"
},
"app.project.install-context.back-to-browse": {
"message": "Retour vers la page découvrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
},
"app.settings.developer-mode-enabled": {
"message": "Mode développeur activé."
},
@@ -5,12 +5,6 @@
"app.auth-servers.unreachable.header": {
"message": "לא ניתן לגשת לשרתי האימות"
},
"app.browse.add-server-to-instance": {
"message": "הוסף שרת להתקנה"
},
"app.browse.add-servers-to-instance": {
"message": "הוסף שרתים להתקנה שלך"
},
"app.browse.add-to-instance": {
"message": "הוספה להתקנה"
},
@@ -29,9 +23,6 @@
"app.browse.discover-servers": {
"message": "גלה שרתים"
},
"app.browse.install-content-to-instance": {
"message": "הוספת התוכן להתקנה"
},
"app.export-modal.description-placeholder": {
"message": "הזן את תיאור חבילת המודים..."
},
@@ -47,9 +38,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "שם חבילת המודים"
},
"app.export-modal.select-files-label": {
"message": "בחר קבצים ותיקיות להכללה בחבילה הזו"
},
"app.export-modal.version-number-label": {
"message": "מספר גרסה"
},
+152 -23
View File
@@ -6,19 +6,97 @@
"message": "Letöltések"
},
"app.action-bar.hide-more-running-instances": {
"message": "További futó példányok elrejtése"
"message": "További futó profilok elrejtése"
},
"app.action-bar.make-primary-instance": {
"message": "Elsődleges példány létrehozása"
"message": "Beállítás elsődleges profilként"
},
"app.action-bar.no-instances-running": {
"message": "Nincsenek futó példányok"
"message": "Nincsenek futó profilok"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Elsődleges példány"
"message": "Elsődleges profil"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó profilok megjelenítése"
},
"app.action-bar.stop-instance": {
"message": "Profil megállítása"
},
"app.action-bar.view-active-downloads": {
"message": "Aktív letöltések megtekintése"
},
"app.action-bar.view-instance": {
"message": "Profil megtekintése"
},
"app.action-bar.view-logs": {
"message": "Naplók megtekintése"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Lehetővé teszi olyan speciális megjelenítési effektusok használatát, mint például az elmosódás, amelyek hardveres gyorsítás nélkül teljesítménycsökkenést okozhatnak."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Fejlett renderelés"
},
"app.appearance-settings.color-theme.description": {
"message": "Válaszd ki a Modrinth Apphoz a kívánt színsémát."
},
"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 Skinek oldalon."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Névcímke eltüntetése"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "A kezdőlap „Folytasd itt” részében a legújabb világok is megtalálhatók."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "„Folytasd itt” a világokat is"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizáld az indítót a Minecraft indításakor."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Indító minimalizálása"
},
"app.appearance-settings.native-decorations.description": {
"message": "A rendszer ablakkeret használata (az alkalmazás újraindítása szükséges)."
},
"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.toggle-sidebar.description": {
"message": "Lehetővé teszi az oldalsáv be- és kikapcsolását."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Oldalsáv megjelenítése/elrejtése"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Ha olyan Modrinth Csomagfájlt (.mrpack) próbálsz telepíteni, amely nem a Modrinth szerverén található, a telepítés előtt gondoskodunk arról, hogy tisztában legyél a kockázatokkal."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Figyelmeztess, mielőtt ismeretlen modcsomagokat telepítenék"
},
"app.auth-servers.unreachable.body": {
"message": "A Minecraft hitelesítő szerverek lehet, hogy nem üzemelnek. Bizonyosodj meg róla, hogy van internetkapcsolatod és próbáld meg újra."
@@ -26,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-server-to-instance": {
"app.browse.add-servers-to-instance": {
"message": "Szerver hozzáadása a profilhoz"
},
"app.browse.add-servers-to-instance": {
"message": "Szerverek hozzáadása a profilodhoz"
"app.browse.add-to-an-instance": {
"message": "Hozzáadás egy profilhoz"
},
"app.browse.add-to-instance": {
"message": "Hozzáadás a profilhoz"
@@ -44,6 +122,9 @@
"app.browse.already-added": {
"message": "Már hozzá van adva"
},
"app.browse.back-to-instance": {
"message": "Vissza a profilhoz"
},
"app.browse.discover-content": {
"message": "Tartalom böngészése"
},
@@ -53,24 +134,21 @@
"app.browse.hide-added-servers": {
"message": "Már hozzáadott szerverek elrejtése"
},
"app.browse.hide-installed-content": {
"message": "Már telepített tartalom elrejtése"
},
"app.browse.install-content-to-instance": {
"message": "Tartalom letöltése a profilhoz"
},
"app.browse.project-type.modpacks": {
"message": "Modcsomagok"
},
"app.browse.server.install": {
"message": "Letöltés"
},
"app.browse.server.installed": {
"message": "Letöltve"
"app.browse.server-instance-content-warning": {
"message": "A tartalom hozzáadása ronthatja a kompatibilitást a szerverhez való csatlakozáskor. A hozzáadott tartalom elveszik, ha frissíted a szerverprofil tartalmát."
},
"app.browse.server.installing": {
"message": "Letöltés..."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modcsomag telepítése..."
},
"app.export-modal.description-placeholder": {
"message": "Írd be a modcsomag leírását..."
},
@@ -80,6 +158,9 @@
"app.export-modal.header": {
"message": "Modcsomag exportálása"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Tartalmaz „{file}” fájlokat?"
},
"app.export-modal.modpack-name-label": {
"message": "A modcsomag neve"
},
@@ -87,7 +168,7 @@
"message": "A modcsomag neve"
},
"app.export-modal.select-files-label": {
"message": "Válaszd ki a csomagba felveendő fájlokat és mappákat"
"message": "Állítsd be, hogy mely fájlok kerüljenek bele az exportba"
},
"app.export-modal.version-number-label": {
"message": "Verziószám"
@@ -216,7 +297,16 @@
"message": "Frissítés szükséges"
},
"app.modal.update-to-play.update-required-description": {
"message": "\nFrissítés szükséges ehhez: {name}. Kérk, frissíts a legújabb verzióra a játék elindításához"
"message": "Frissítés szükséges ehhez: {name}. Kérlek, frissíts a legújabb verzióra a játék elindításához."
},
"app.project.install-button.already-installed": {
"message": "Ez a projekt már telepítve van"
},
"app.project.install-context.back-to-browse": {
"message": "Vissza a böngészéshez"
},
"app.project.install-context.install-content-to-instance": {
"message": "Tartalom telepítése a profilba"
},
"app.settings.developer-mode-enabled": {
"message": "Fejlesztői mód bekapcsolva."
@@ -243,10 +333,10 @@
"message": "Erőforráskezelés"
},
"app.update-popup.body": {
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás bezárásakor automatikusan frissül."
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
},
"app.update-popup.body.download-complete": {
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás bezárásakor automatikusan frissül."
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
},
"app.update-popup.body.linux": {
"message": "A Modrinth App v{version} elérhető. Használd a csomagkezelőt a legújabb funkciók és javítások frissítéséhez!"
@@ -579,7 +669,7 @@
"message": "Ne mutasd a kezdőlapon"
},
"instance.worlds.game_already_open": {
"message": "A profil már meg van nyitva"
"message": "A profil már el van indítva"
},
"instance.worlds.hardcore": {
"message": "Hardcore mód"
@@ -608,6 +698,24 @@
"instance.worlds.world_in_use": {
"message": "A világ használatban van"
},
"minecraft-account.add-account": {
"message": "Fiók hozzáadása"
},
"minecraft-account.label": {
"message": "Minecraft-fiók"
},
"minecraft-account.not-signed-in": {
"message": "Nem vagy bejelentkezve"
},
"minecraft-account.remove-account": {
"message": "Fiók törlése"
},
"minecraft-account.select-account": {
"message": "Fiók kiválasztása"
},
"minecraft-account.sign-in": {
"message": "Bejelentkezés a Minecraftba"
},
"search.filter.locked.instance": {
"message": "A profil által van megadva"
},
@@ -631,5 +739,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "A betöltőt a szerver biztosítja"
},
"unknown-pack-warning-modal.body": {
"message": "Egy fájlt csak akkor vizsgálunk meg, ha azt feltöltik a Modrinthra, függetlenül a fájlformátumtól (beleértve a .mrpack formátumot is)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Ne mutasd újra ezt a figyelmeztetést"
},
"unknown-pack-warning-modal.header": {
"message": "Telepítés megerősítése"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Letöltés mindenképpen"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "A rosszindulatú programokat gyakran modcsomag-fájlokon keresztül terjesztik, például a Discordhoz hasonló platformokon történő megosztás révén."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ezt a fájlt nem találtuk meg a Modrinthon. Határozottan javasoljuk, hogy kizárólag megbízható forrásokból származó fájlokat telepíts."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Figyelmeztetés ismeretlen fájlról"
}
}
+80 -14
View File
@@ -3,25 +3,73 @@
"message": "Mengunduh Java {version}"
},
"app.action-bar.downloads": {
"message": "Mengunduh"
"message": "Unduhan"
},
"app.action-bar.hide-more-running-instances": {
"message": "Sembunyikan lebih banyak instance yang sedang berjalan"
"message": "Sembunyikan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.make-primary-instance": {
"message": "Jadikan sebagai instance utama"
"message": "Jadikan sebagai instans utama"
},
"app.action-bar.no-instances-running": {
"message": "Tidak ada instance yang sedang berjalan"
"message": "Tidak ada instans yang sedang berjalan"
},
"app.action-bar.offline": {
"message": "Offline"
"message": "Luring"
},
"app.action-bar.primary-instance": {
"message": "Instance utama"
"message": "Instans utama"
},
"app.action-bar.show-more-running-instances": {
"message": "Tampilkan lebih banyak instance yang sedang berjalan"
"message": "Tampilkan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.stop-instance": {
"message": "Hentikan instans"
},
"app.action-bar.view-active-downloads": {
"message": "Lihat pengunduhan berlangsung"
},
"app.action-bar.view-instance": {
"message": "Lihat instans"
},
"app.action-bar.view-logs": {
"message": "Lihat catatan"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Menghidupkan renderasi tingkat lanjut seperti efek buram yang dapat menyebabkan masalah kinerja tanpa renderasi yang dipercepat perangkat keras."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderasi tingkat lanjut"
},
"app.appearance-settings.color-theme.description": {
"message": "Pilih tema warna pilihan Anda untuk Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema warna"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ubah halaman utama yang dibuka oleh peluncur."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Beranda"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Pustaka"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Mematikan label nama di atas wujud pemain Anda pada halaman rupa."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Sembunyikan label nama"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Kecilkan pluncur ketika proses Minecraft berjalan."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Kecilkan peluncur"
},
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.auth-servers.unreachable.body": {
"message": "Server autentikasi Minecraft mungkin sedang tidak tersedia saat ini. Periksa koneksi internet Anda dan coba lagi nanti."
@@ -29,11 +77,11 @@
"app.auth-servers.unreachable.header": {
"message": "Tidak dapat terhubung ke server autentikasi"
},
"app.browse.add-server-to-instance": {
"message": "Tambah server ke instans"
},
"app.browse.add-servers-to-instance": {
"message": "Tambah server ke instans Anda"
"message": "Menambah server ke instans"
},
"app.browse.add-to-an-instance": {
"message": "Tambah ke instans"
},
"app.browse.add-to-instance": {
"message": "Tambah ke instans"
@@ -47,14 +95,29 @@
"app.browse.already-added": {
"message": "Telah ditambahkan"
},
"app.browse.back-to-instance": {
"message": "Kembali ke instans"
},
"app.browse.discover-content": {
"message": "Temukan konten"
},
"app.browse.discover-servers": {
"message": "Temukan server"
},
"app.browse.install-content-to-instance": {
"message": "Pasang konten ke instans"
"app.browse.hide-added-servers": {
"message": "Sembunyikan server yang sudah ditambah"
},
"app.browse.project-type.modpacks": {
"message": "Paket Mod"
},
"app.browse.server.installing": {
"message": "Memasang"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Memasang paket mod..."
},
"app.export-modal.description-placeholder": {
"message": "Masukkan deskripsi paket mod..."
@@ -65,6 +128,9 @@
"app.export-modal.header": {
"message": "Ekspor paket mod"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sertakan \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nama Paket Mod"
},
@@ -72,7 +138,7 @@
"message": "Nama paket mod"
},
"app.export-modal.select-files-label": {
"message": "Pilih berkas dan folder yang ingin dimasukkan ke paket"
"message": "Konfigurasikan berkas mana yang disertakan dalam pengeksporan ini"
},
"app.export-modal.version-number-label": {
"message": "Nomor versi"
+43 -37
View File
@@ -1,12 +1,12 @@
{
"app.action-bar.downloading-java": {
"message": "Scaricando Java {version}"
"message": "Java {version}"
},
"app.action-bar.downloads": {
"message": "Download"
},
"app.action-bar.hide-more-running-instances": {
"message": "Nascondi le istanze in esecuzione"
"message": "Mostra meno istanze in esecuzione"
},
"app.action-bar.make-primary-instance": {
"message": "Rendi primaria"
@@ -21,19 +21,19 @@
"message": "Primaria"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostra le istanze in esecuzione"
"message": "Mostra più istanze in esecuzione"
},
"app.action-bar.stop-instance": {
"message": "Ferma l'istanza"
},
"app.action-bar.view-active-downloads": {
"message": "Vedi i download attivi"
"message": "Mostra i download attivi"
},
"app.action-bar.view-instance": {
"message": "Vedi l'istanza"
"message": "Mostra l'istanza"
},
"app.action-bar.view-logs": {
"message": "Vedi i log"
"message": "Mostra i log"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Abilita alcuni effetti come la sfocatura, ma può causare problemi di prestazioni senza accelerazione hardware."
@@ -63,28 +63,28 @@
"message": "Rimuove il nametag sopra la testa del giocatore nella pagina delle skin."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Nascondi nametag"
"message": "Nascondi il nome"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Aggiunge i mondi recenti nella sezione \"Avvio rapido\" della pagina Home."
"message": "Elenca i mondi recenti nella sezione \"Avvio rapido\" della pagina Home."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Avvio rapido nei mondi"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Riduci il launcher quanto si avvia Minecraft."
"message": "Riduci il launcher a icona quando si avvia Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Riduci il launcher"
"message": "Riduci a icona"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa la cornice di sistema (riavvio richiesto)."
"message": "Usa la cornice di sistema (riavvio necessario)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Seleziona un'opzione"
"message": "Scegli una pagina"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Scegli se mostrare o nascondere la barra laterale."
@@ -93,10 +93,10 @@
"message": "Abilita la barra laterale"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, così da conoscerne i rischi."
"message": "Visti i rischi, apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami prima di installare pacchetti sconosciuti"
"message": "Avvisami se installo pacchetti sconosciuti"
},
"app.auth-servers.unreachable.body": {
"message": "I server di autenticazione di Minecraft stanno riscontrando problemi. Controlla la tua connessione a Internet e riprova più tardi."
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Impossibile raggiungere i server di autenticazione"
},
"app.browse.add-server-to-instance": {
"message": "Aggiungi server all'istanza"
},
"app.browse.add-servers-to-instance": {
"message": "Aggiungi i server alla tua istanza"
"message": "Aggiungendo il server all'istanza"
},
"app.browse.add-to-an-instance": {
"message": "Aggiungi a un'istanza"
},
"app.browse.add-to-instance": {
"message": "Aggiungi all'istanza"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Già aggiunto"
},
"app.browse.back-to-instance": {
"message": "Torna all'istanza"
},
"app.browse.discover-content": {
"message": "Esplora i contenuti"
},
@@ -131,23 +134,14 @@
"app.browse.hide-added-servers": {
"message": "Nascondi server già aggiunti"
},
"app.browse.hide-installed-content": {
"message": "Nascondi contenuti già installati"
},
"app.browse.install-content-to-instance": {
"message": "Installa contenuti nell'istanza"
},
"app.browse.project-type.modpacks": {
"message": "Pacchetti di mod"
},
"app.browse.server.install": {
"message": "Installa"
},
"app.browse.server.installed": {
"message": "Installato"
"app.browse.server-instance-content-warning": {
"message": "Aggiungere dei contenuti potrebbe portare problemi entrando nel server. Qualsiasi contenuto aggiunto sarà anche perso aggiornando i contenuti dell'istanza del server."
},
"app.browse.server.installing": {
"message": "Installando"
"message": "Installazione"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Esporta pacchetto"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Includere \"{file}\" nell'esportazione?"
},
"app.export-modal.modpack-name-label": {
"message": "Nome del pacchetto"
},
@@ -171,7 +168,7 @@
"message": "Nome del pacchetto"
},
"app.export-modal.select-files-label": {
"message": "Seleziona i file e le cartelle da includere nel pacchetto"
"message": "Seleziona i file da includere in questa esportazione"
},
"app.export-modal.version-number-label": {
"message": "Numero di versione"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name} richiede degli aggiornamenti. Installa l'ultima versione per poter giocare."
},
"app.project.install-button.already-installed": {
"message": "Questo progetto è già stato installato"
},
"app.project.install-context.back-to-browse": {
"message": "Torna a Esplora"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installa il contenuto nell'istanza"
},
"app.settings.developer-mode-enabled": {
"message": "Modalità sviluppatore attiva."
},
@@ -441,7 +447,7 @@
"message": "Nessuna amicizia corrisponde a ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cerca amicizie..."
"message": "Cerca tra le amicizie..."
},
"friends.section.heading": {
"message": "{title} - {count}"
@@ -480,7 +486,7 @@
"message": "Mondo di Minecraft"
},
"instance.edit-world.reset-icon": {
"message": "Resetta icona"
"message": "Ripristina icona"
},
"instance.edit-world.title": {
"message": "Modifica mondo"
@@ -540,7 +546,7 @@
"message": "Cambia icona"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Seleziona icona"
"message": "Scegli un'icona"
},
"instance.settings.tabs.general.library-groups": {
"message": "Gruppi della libreria"
@@ -735,7 +741,7 @@
"message": "Il loader è determinato dal server"
},
"unknown-pack-warning-modal.body": {
"message": "Un file è esaminato quando è caricato su Modrinth, indipendentemente dal suo formato (.mrpack inclusi)."
"message": "Solo i file caricati su Modrinth vengono esaminati, qualunque sia il loro formato (.mrpack inclusi)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Non mostrare più questo avviso"
@@ -747,10 +753,10 @@
"message": "Installa comunque"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Molto spesso i malware sono distribuiti attraverso i pacchetti di mod, distribuiti in piattaforme come Discord."
"message": "Spesso i malware vengono nascosti nei pacchetti di mod, poi distribuiti su piattaforme come Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Non siamo riusciti a trovare questo file su Modrinth. Consigliamo di installare file solo da fonti di cui ti fidi."
"message": "Non è stato possibile trovare questo file su Modrinth. Consigliamo di installare file solo da fonti attendibili."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Tipo di file sconosciuto"
+18 -21
View File
@@ -104,12 +104,6 @@
"app.auth-servers.unreachable.header": {
"message": "認証サーバーにアクセスできません"
},
"app.browse.add-server-to-instance": {
"message": "サーバーをインスタンスに追加"
},
"app.browse.add-servers-to-instance": {
"message": "サーバーを自身のインスタンスに追加"
},
"app.browse.add-to-instance": {
"message": "インスタンスに追加"
},
@@ -131,21 +125,9 @@
"app.browse.hide-added-servers": {
"message": "既に追加済みのサーバーを非表示にする"
},
"app.browse.hide-installed-content": {
"message": "既にインストールされているコンテンツを非表示にする"
},
"app.browse.install-content-to-instance": {
"message": "コンテンツをインスタンスにインストールする"
},
"app.browse.project-type.modpacks": {
"message": "Modパック"
},
"app.browse.server.install": {
"message": "インストール"
},
"app.browse.server.installed": {
"message": "インストール済み"
},
"app.browse.server.installing": {
"message": "インストール中"
},
@@ -170,9 +152,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modパック名"
},
"app.export-modal.select-files-label": {
"message": "パックに含めるファイルとフォルダーを選択"
},
"app.export-modal.version-number-label": {
"message": "バージョン番号"
},
@@ -692,6 +671,24 @@
"instance.worlds.world_in_use": {
"message": "ワールドは使用中"
},
"minecraft-account.add-account": {
"message": "アカウントを追加"
},
"minecraft-account.label": {
"message": "Minecraftアカウント"
},
"minecraft-account.not-signed-in": {
"message": "サインインしていません"
},
"minecraft-account.remove-account": {
"message": "アカウントを一覧から削除"
},
"minecraft-account.select-account": {
"message": "アカウントを選択"
},
"minecraft-account.sign-in": {
"message": "Minecraftにサインイン"
},
"search.filter.locked.instance": {
"message": "インスタンスによる条件"
},
+39 -15
View File
@@ -104,12 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "인증 서버에 연결할 수 없습니다"
},
"app.browse.add-server-to-instance": {
"message": "인스턴스에 서버 추가"
},
"app.browse.add-servers-to-instance": {
"message": "인스턴스에 서버 추가"
},
"app.browse.add-to-an-instance": {
"message": "인스턴스에 추가"
},
"app.browse.add-to-instance": {
"message": "인스턴스에 추가"
},
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "이미 추가됨"
},
"app.browse.back-to-instance": {
"message": "인스턴스로 돌아가기"
},
"app.browse.discover-content": {
"message": "콘텐츠 탐색하기"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "이미 추가된 서버 숨기기"
},
"app.browse.hide-installed-content": {
"message": "이미 설치된 콘텐츠 숨기기"
},
"app.browse.install-content-to-instance": {
"message": "인스턴스에 콘텐츠 설치"
},
"app.browse.project-type.modpacks": {
"message": "모드팩"
},
"app.browse.server.install": {
"message": "설치"
},
"app.browse.server.installed": {
"message": "설치됨"
"app.browse.server-instance-content-warning": {
"message": "콘텐츠를 추가하면 서버 접속 시 호환이 안 될 수 있습니다. 또한 서버 인스턴스 콘텐츠를 업데이트하면 임의로 추가된 콘텐츠는 모두 손실됩니다."
},
"app.browse.server.installing": {
"message": "설치 중"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "모드팩 내보내기"
},
"app.export-modal.include-file-accessibility-label": {
"message": "\"{file}\"(을)를 포함할까요?"
},
"app.export-modal.modpack-name-label": {
"message": "모드팩 이름"
},
@@ -171,7 +168,7 @@
"message": "모드팩 이름"
},
"app.export-modal.select-files-label": {
"message": "팩에 포함할 파일과 폴더 선택"
"message": "내보내기에 어느 파일이 포함될지 구성"
},
"app.export-modal.version-number-label": {
"message": "버전 구분"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name}을(를) 플레이하려면 업데이트가 필요합니다. 게임을 실행하려면 최신 버전으로 업데이트해 주세요."
},
"app.project.install-button.already-installed": {
"message": "이미 설치되어 있음"
},
"app.project.install-context.back-to-browse": {
"message": "탐색으로 돌아가기"
},
"app.project.install-context.install-content-to-instance": {
"message": "인스턴스에 콘텐츠 설치"
},
"app.settings.developer-mode-enabled": {
"message": "개발자 모드가 활성화되었습니다."
},
@@ -692,6 +698,24 @@
"instance.worlds.world_in_use": {
"message": "사용 중인 세계"
},
"minecraft-account.add-account": {
"message": "계정 추가"
},
"minecraft-account.label": {
"message": "마인크래프트 계정"
},
"minecraft-account.not-signed-in": {
"message": "로그인하지 않음"
},
"minecraft-account.remove-account": {
"message": "계정 제거"
},
"minecraft-account.select-account": {
"message": "계정 선택"
},
"minecraft-account.sign-in": {
"message": "마인크래프트 계정으로 로그인"
},
"search.filter.locked.instance": {
"message": "인스턴스에서 관리"
},
+163 -7
View File
@@ -5,20 +5,110 @@
"app.action-bar.downloads": {
"message": "Muat Turun"
},
"app.action-bar.hide-more-running-instances": {
"message": "Sembunyikan lebih banyak pemasangan yang berjalan"
},
"app.action-bar.make-primary-instance": {
"message": "Jadikan pemasangan utama"
},
"app.action-bar.no-instances-running": {
"message": "Tiada pemasangan yang berjalan"
},
"app.action-bar.offline": {
"message": "Luar Talian"
},
"app.action-bar.primary-instance": {
"message": "Pemasangan utama"
},
"app.action-bar.show-more-running-instances": {
"message": "Tunjukkan lebih banyak pemasangan yang berjalan"
},
"app.action-bar.stop-instance": {
"message": "Hentikan pemasangan"
},
"app.action-bar.view-active-downloads": {
"message": "Lihat muat turun aktif"
},
"app.action-bar.view-instance": {
"message": "Lihat pemasangan"
},
"app.action-bar.view-logs": {
"message": "Lihat log"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Mendayakan pemaparan lanjutan seperti kesan kabur yang boleh menyebabkan masalah prestasi tanpa pemaparan dipercepatkan perkakasan."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Pemaparan lanjutan"
},
"app.appearance-settings.color-theme.description": {
"message": "Pilih tema warna pilihan anda untuk Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema warna"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Tukar laman utama yang dibuka oleh pelancar."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Laman utama"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Pustaka"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Laman pendaratan lalai"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Menyahdayakan tanda nama di atas pemain anda pada halaman kekulit."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Sembunyikan tanda nama"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Sertakan dunia terkini dalam bahagian \"Lompat kembali\" pada Laman Utama."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Lompat kembali ke dalam dunia"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Meminimumkan pelancar apabila proses Minecraft bermula."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimumkan pelancar"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gunakan bingkai tetingkap sistem (aplikasi perlu dimulakan semula)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih satu pilihan"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Membolehkan keupayaan untuk menogol bar sisi."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Togol bar sisi"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Jika anda cuba memasang fail Modrinth Pack (.mrpack) yang tidak dihoskan pada Modrinth, kami akan memastikan anda memahami risikonya sebelum memasangnya."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Beri saya amaran sebelum memasang pek mod yang tidak diketahui"
},
"app.auth-servers.unreachable.body": {
"message": "Pelayan pengesahan Minecraft mungkin sedang tergendala sekarang. Periksa sambungan internet anda dan cuba lagi kemudian."
},
"app.auth-servers.unreachable.header": {
"message": "Tidak dapat mencapai pelayan pengesahan"
},
"app.browse.add-server-to-instance": {
"message": "Tambahkan pelayan ke dalam pemasangan"
},
"app.browse.add-servers-to-instance": {
"message": "Tambahkan pelayan ke dalam pemasangan anda"
"message": "Menambah pelayan ke dalam pemasangan"
},
"app.browse.add-to-an-instance": {
"message": "Tambahkan ke dalam pemasangan"
},
"app.browse.add-to-instance": {
"message": "Tambahkan ke dalam pemasangan"
@@ -32,14 +122,32 @@
"app.browse.already-added": {
"message": "Sudah ditambah"
},
"app.browse.back-to-instance": {
"message": "Kembali ke pemasangan"
},
"app.browse.discover-content": {
"message": "Temui kandungan"
},
"app.browse.discover-servers": {
"message": "Temui pelayan"
},
"app.browse.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
"app.browse.hide-added-servers": {
"message": "Sembunyikan pelayan yang sudah ditambah"
},
"app.browse.project-type.modpacks": {
"message": "Pek Mod"
},
"app.browse.server-instance-content-warning": {
"message": "Menambah kandungan boleh merosakkan keserasian apabila menyertai pelayan. Sebarang kandungan yang ditambah juga akan hilang apabila anda mengemas kini kandungan pemasangan pelayan."
},
"app.browse.server.installing": {
"message": "Sedang memasang"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Sedang memasang pek mod..."
},
"app.export-modal.description-placeholder": {
"message": "Masukkan keterangan pek mod..."
@@ -50,6 +158,9 @@
"app.export-modal.header": {
"message": "Eksport pek mod"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sertakan \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nama Pek Mod"
},
@@ -57,7 +168,7 @@
"message": "Nama pek mod"
},
"app.export-modal.select-files-label": {
"message": "Pilih fail dan folder untuk disertakan dalam pek mod"
"message": "Konfigurasikan fail yang disertakan dalam eksport ini"
},
"app.export-modal.version-number-label": {
"message": "Nombor versi"
@@ -188,6 +299,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "Kemas kini diperlukan untuk memainkan {name}. Sila kemas kini kepada versi terkini untuk melancarkan permainan."
},
"app.project.install-button.already-installed": {
"message": "Projek ini sudah dipasang"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang kandungan ke dalam pemasangan"
},
"app.settings.developer-mode-enabled": {
"message": "Mod pembangun didayakan."
},
@@ -578,6 +695,24 @@
"instance.worlds.world_in_use": {
"message": "Dunia sedang digunakan"
},
"minecraft-account.add-account": {
"message": "Tambah akaun"
},
"minecraft-account.label": {
"message": "Akaun Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Tidak didaftar masuk"
},
"minecraft-account.remove-account": {
"message": "Alih keluar akaun"
},
"minecraft-account.select-account": {
"message": "Pilih akaun"
},
"minecraft-account.sign-in": {
"message": "Daftar masuk ke dalam Minecraft"
},
"search.filter.locked.instance": {
"message": "Disediakan oleh pemasangan ini"
},
@@ -601,5 +736,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Pemuat adalah disediakan oleh pelayan"
},
"unknown-pack-warning-modal.body": {
"message": "Sesuatu fail hanya disemak jika ia dimuat naik ke Modrinth, tanpa mengira format failnya (termasuk .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Jangan tunjukkan amaran ini lagi"
},
"unknown-pack-warning-modal.header": {
"message": "Sahkan pemasangan"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Pasangkan juga"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Perisian hasad sering diedarkan melalui fail pek mod dengan berkongsinya di platform seperti Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Kami tidak dapat menemui fail ini di Modrinth. Kami sangat mengesyorkan anda untuk hanya memasang fail daripada sumber yang anda percayai."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Amaran fail tidak diketahui"
}
}
+172 -7
View File
@@ -1,15 +1,114 @@
{
"app.action-bar.downloading-java": {
"message": "Java {version} aan het downloaden"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Verberg meer actieve instanties"
},
"app.action-bar.make-primary-instance": {
"message": "Maak primaire instantie"
},
"app.action-bar.no-instances-running": {
"message": "Geen actieve instanties"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primaire instantie"
},
"app.action-bar.show-more-running-instances": {
"message": "Toon meer actieve instanties"
},
"app.action-bar.stop-instance": {
"message": "Stop instantie"
},
"app.action-bar.view-active-downloads": {
"message": "Bekijk actieve downloads"
},
"app.action-bar.view-instance": {
"message": "Bekijk instantie"
},
"app.action-bar.view-logs": {
"message": "Bekijk logs"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Activeert geavanceerde weergaves zoals blur effecten die mogelijke prestatieproblemen opleveren zonder hardware-versnelde weergeving."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Geavanceerde weergave"
},
"app.appearance-settings.color-theme.description": {
"message": "Selecteer je voorkeur kleurenthema voor de Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Kleurenthema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Wijzig de pagina waarop de launcher opent."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotheek"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Standaard openingspagina"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Schakelt het nametag boven jouw speler op de skins pagina uit."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Verberg nametag"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Inclusief recente werelden in het gedeelte \"Ga terug\" op de startpagina."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Ga terug in werelden"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimaliseer de launcher als een Minecraft proces wordt gestart."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimaliseer launcher"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gebruik systeem venster frame (app opnieuw starten vereist)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Oorspronkelijke versieringen"
},
"app.appearance-settings.select-option": {
"message": "Selecteer een optie"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Schakelt de mogelijkheid om de zijbalk in- of uit te schakelen in."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Schakel zijbalk"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Als je probeert een Modrinth Pack-bestand (.mrpack) te installeren dat niet op Modrinth zelf wordt gehost, zorgen we ervoor dat je de risico's begrijpt voordat je het installeert."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Waarschuw mij voordat ik onbekende modpacks installeer"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraft authenticatie servers zijn mogelijk offline. Controleer je internetverbinding en probeer opnieuw later."
},
"app.auth-servers.unreachable.header": {
"message": "Authenticatieservers kunnen niet worden bereikt"
},
"app.browse.add-server-to-instance": {
"message": "Voeg server aan instantie toe"
},
"app.browse.add-servers-to-instance": {
"message": "Voeg servers aan instantie toe"
"message": "Server toevoegen aan instantie"
},
"app.browse.add-to-an-instance": {
"message": "Voeg toe aan een instantie"
},
"app.browse.add-to-instance": {
"message": "Voeg aan instantie toe"
@@ -23,14 +122,32 @@
"app.browse.already-added": {
"message": "Al toegevoegd"
},
"app.browse.back-to-instance": {
"message": "Terug naar instantie"
},
"app.browse.discover-content": {
"message": "Ontdek content"
},
"app.browse.discover-servers": {
"message": "Ontdek servers"
},
"app.browse.install-content-to-instance": {
"message": "Installeer inhoud naar instantie"
"app.browse.hide-added-servers": {
"message": "Verberg al toegevoegde servers"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
},
"app.browse.server-instance-content-warning": {
"message": "Het toevoegen van content kan de compatibiliteit verbreken bij het verbinden met de server. Alle toegevoegde inhoud gaat ook verloren wanneer u de content van de serverinstantie bijwerkt."
},
"app.browse.server.installing": {
"message": "Installeren"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modpack aan het installeren..."
},
"app.export-modal.description-placeholder": {
"message": "Voeg modpack beschrijving in..."
@@ -41,6 +158,9 @@
"app.export-modal.header": {
"message": "Exporteer modpack"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Betrek \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Modpack Naam"
},
@@ -48,7 +168,7 @@
"message": "Modpack naam"
},
"app.export-modal.select-files-label": {
"message": "Selecteer bestanden en mappen om toe te voegen aan pack"
"message": "Configureer welke bestanden bij deze export zijn betrokken"
},
"app.export-modal.version-number-label": {
"message": "Versie nummer"
@@ -179,6 +299,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "Een update is vereist om {name} te spelen. Update naar de laatste versie om het spel te starten."
},
"app.project.install-button.already-installed": {
"message": "Dit project is al geïnstalleerd"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installeer content naar instantie"
},
"app.settings.developer-mode-enabled": {
"message": "Ontwikkelaarsmodus ingeschakeld."
},
@@ -569,6 +695,24 @@
"instance.worlds.world_in_use": {
"message": "Wereld wordt gebruikt"
},
"minecraft-account.add-account": {
"message": "Voeg account toe"
},
"minecraft-account.label": {
"message": "Minecraft account"
},
"minecraft-account.not-signed-in": {
"message": "Niet ingelogd"
},
"minecraft-account.remove-account": {
"message": "Verwijder account"
},
"minecraft-account.select-account": {
"message": "Selecteer account"
},
"minecraft-account.sign-in": {
"message": "Log in bij Minecraft"
},
"search.filter.locked.instance": {
"message": "Aangeboden door de instantie"
},
@@ -592,5 +736,26 @@
},
"search.filter.locked.server-loader.title": {
"message": "Loader is gegeven door de server"
},
"unknown-pack-warning-modal.body": {
"message": "Een bestand wordt alleen beoordeeld als het naar Modrinth is geüpload, ongeacht het bestandsformaat (inclusief .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Toon deze waarschuwing niet opnieuw"
},
"unknown-pack-warning-modal.header": {
"message": "Bevestig installatie"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Installeer toch"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Malware wordt vaak verspreid via modpack-bestanden door ze te delen op platforms zoals Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "We konden dit bestand niet vinden op Modrinth. We raden ten zeerste aan om alleen bestanden te installeren van bronnen die u vertrouwt."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Waarschuwing voor onbekend bestand"
}
}
+41 -17
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Nie udało się połączyć się z serwerami uwierzytelniania"
},
"app.browse.add-server-to-instance": {
"message": "Dodaj serwer do instancji"
},
"app.browse.add-servers-to-instance": {
"message": "Dodaj serwery do swojej instancji"
"message": "Dodawanie serwera do instancji"
},
"app.browse.add-to-an-instance": {
"message": "Dodaj do instancji"
},
"app.browse.add-to-instance": {
"message": "Dodaj do instancji"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Już dodano"
},
"app.browse.back-to-instance": {
"message": "Powrót do instancji"
},
"app.browse.discover-content": {
"message": "Odkrywaj zawartość"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "Ukryj już dodane serwery"
},
"app.browse.hide-installed-content": {
"message": "Ukryj już dodane zasoby"
},
"app.browse.install-content-to-instance": {
"message": "Zainstaluj zasoby do instancji"
},
"app.browse.project-type.modpacks": {
"message": "Paczki modów"
},
"app.browse.server.install": {
"message": "Zainstaluj"
},
"app.browse.server.installed": {
"message": "Zainstalowano"
"app.browse.server-instance-content-warning": {
"message": "Dodawanie zawartości może zepsuć kompatybilność podczas dołączania na serwer. Jakakolwiek dodana zawartość zostanie stracona, gdy zaktualizujesz zawartość instancji serwera."
},
"app.browse.server.installing": {
"message": "Instalowanie"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Eksportuj paczkę modów"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Dołączyć \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nazwa paczki modów"
},
@@ -171,7 +168,7 @@
"message": "Nazwa paczki modów"
},
"app.export-modal.select-files-label": {
"message": "Wybierz pliki i foldery, które mają znaleźć się w paczce"
"message": "Skonfiguruje jakie pliki zostaną załączone w tym eksporcie"
},
"app.export-modal.version-number-label": {
"message": "Numer wersji"
@@ -180,7 +177,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i wszystkie zainstalowane treści."
"message": "Wszystkie dane z Twojej instancji zostaną trwale usunięte, w tym Twoje światy, pliki konfiguracji i jakakolwiek dodana zawartość."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Tej akcji nie można odwrócić"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Aktualizacja jest wymagana, aby grać w {name}. Proszę zaktualizować do najnowszej wersji, aby uruchomić grę."
},
"app.project.install-button.already-installed": {
"message": "Ten projekt jest już zainstalowany"
},
"app.project.install-context.back-to-browse": {
"message": "Powróć do odkrywania"
},
"app.project.install-context.install-content-to-instance": {
"message": "Zainstaluj zasoby do instancji"
},
"app.settings.developer-mode-enabled": {
"message": "Tryb dewelopera włączony."
},
@@ -692,6 +698,24 @@
"instance.worlds.world_in_use": {
"message": "Świat jest w użytku"
},
"minecraft-account.add-account": {
"message": "Dodaj konto"
},
"minecraft-account.label": {
"message": "Konto Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Nie zalogowano"
},
"minecraft-account.remove-account": {
"message": "Usuń konto"
},
"minecraft-account.select-account": {
"message": "Wybierz konto"
},
"minecraft-account.sign-in": {
"message": "Zaloguj się do Minecraft"
},
"search.filter.locked.instance": {
"message": "Podane przez instancję"
},
+22 -16
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Não foi possível acessar os servidores de autenticação"
},
"app.browse.add-server-to-instance": {
"message": "Adicionar servidor à instância"
},
"app.browse.add-servers-to-instance": {
"message": "Adicionar servidores à sua instância"
"message": "Adicionando servidor à instância"
},
"app.browse.add-to-an-instance": {
"message": "Adicionar a uma instância"
},
"app.browse.add-to-instance": {
"message": "Adicionar à instância"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Já adicionado"
},
"app.browse.back-to-instance": {
"message": "Voltar a instância"
},
"app.browse.discover-content": {
"message": "Descubra conteúdo"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "Ocultar servidores já adicionados"
},
"app.browse.hide-installed-content": {
"message": "Ocultar conteúdo já adicionado"
},
"app.browse.install-content-to-instance": {
"message": "Instalar conteúdo na instância"
},
"app.browse.project-type.modpacks": {
"message": "Pacotes de mods"
},
"app.browse.server.install": {
"message": "Instalar"
},
"app.browse.server.installed": {
"message": "Instalado"
"app.browse.server-instance-content-warning": {
"message": "Adicionar conteúdo pode quebrar a compatibilidade ao entrar no servidor. Qualquer conteúdo adicionado também será perdido ao atualizar o conteúdo da instância do servidor."
},
"app.browse.server.installing": {
"message": "Instalando"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Exportar pacote de mods"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Incluir \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nome do pacote de mods"
},
@@ -171,7 +168,7 @@
"message": "Nome do pacote de mods"
},
"app.export-modal.select-files-label": {
"message": "Selecione arquivos e pastas para incluir no pacote"
"message": "Configure quais arquivos serão incluídos nessa exportação"
},
"app.export-modal.version-number-label": {
"message": "Número da versão"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "É necessária uma atualização para jogar {name}. Atualize para a versão mais recente para iniciar o jogo."
},
"app.project.install-button.already-installed": {
"message": "Este projeto já foi instalado"
},
"app.project.install-context.back-to-browse": {
"message": "Voltar ao descobrir"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar conteúdo para a instância"
},
"app.settings.developer-mode-enabled": {
"message": "Modo de desenvolvedor ativado."
},
+57 -12
View File
@@ -1,16 +1,67 @@
{
"app.action-bar.downloading-java": {
"message": "A descarregar a versão do java {version}"
},
"app.action-bar.downloads": {
"message": "Downloads"
},
"app.action-bar.hide-more-running-instances": {
"message": "Esconder mais instâncias a correr"
},
"app.action-bar.make-primary-instance": {
"message": "Transformar na instância principal"
},
"app.action-bar.no-instances-running": {
"message": "Nenhuma instância a correr"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Instância principal"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostrar mais instâncias a correr"
},
"app.action-bar.stop-instance": {
"message": "Para instância"
},
"app.action-bar.view-active-downloads": {
"message": "Ver instâncias ativas"
},
"app.action-bar.view-instance": {
"message": "Ver instância"
},
"app.action-bar.view-logs": {
"message": "Ver logs"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderização avançada"
},
"app.appearance-settings.color-theme.description": {
"message": "Seleciona a tua cor preferida para a Modrinth App."
},
"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.auth-servers.unreachable.body": {
"message": "Os servidores de autenticação do Minecraft poderão estar em baixo de momento. Verifica a tua ligação à internet e tenta novamente mais tarde."
},
"app.auth-servers.unreachable.header": {
"message": "Não foi possível alcançar os servidores de autenticação"
},
"app.browse.add-server-to-instance": {
"message": "Adicionar servidor à instância"
},
"app.browse.add-servers-to-instance": {
"message": "Adicionar servidores à tua instância"
},
"app.browse.add-to-instance": {
"message": "Adicionar à instância"
},
@@ -29,9 +80,6 @@
"app.browse.discover-servers": {
"message": "Descobrir servidores"
},
"app.browse.install-content-to-instance": {
"message": "Instalar conteúdo à instância"
},
"app.export-modal.description-placeholder": {
"message": "Insere a descrição do modpack..."
},
@@ -47,9 +95,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Nome do Modpack"
},
"app.export-modal.select-files-label": {
"message": "Seleciona as pastas e ficheiros a incluir no pack"
},
"app.export-modal.version-number-label": {
"message": "Número da versão"
},
+41 -17
View File
@@ -3,7 +3,7 @@
"message": "Скачивание Java {version}"
},
"app.action-bar.downloads": {
"message": "Скачивания"
"message": "Скачивание"
},
"app.action-bar.hide-more-running-instances": {
"message": "Скрыть другие активные сборки"
@@ -104,12 +104,12 @@
"app.auth-servers.unreachable.header": {
"message": "Нет связи с серверами аутентификации"
},
"app.browse.add-server-to-instance": {
"message": "Добавить сервер в сборку"
},
"app.browse.add-servers-to-instance": {
"message": "Добавление серверов в сборку"
},
"app.browse.add-to-an-instance": {
"message": "Добавить в сборку"
},
"app.browse.add-to-instance": {
"message": "Добавить в сборку"
},
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Уже добавлено"
},
"app.browse.back-to-instance": {
"message": "Вернуться к сборке"
},
"app.browse.discover-content": {
"message": "Поиск проектов"
},
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "Скрывать добавленные"
},
"app.browse.hide-installed-content": {
"message": "Скрывать установленные"
},
"app.browse.install-content-to-instance": {
"message": "Установка контента в сборку"
},
"app.browse.project-type.modpacks": {
"message": "Сборки"
},
"app.browse.server.install": {
"message": "Установить"
},
"app.browse.server.installed": {
"message": "Установлено"
"app.browse.server-instance-content-warning": {
"message": "Добавление контента может нарушить совместимость при подключении к серверу. Также весь добавленный контент будет удалён при обновлении содержимого серверной сборки."
},
"app.browse.server.installing": {
"message": "Установка"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Экспорт сборки"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Добавить «{file}» в экспорт?"
},
"app.export-modal.modpack-name-label": {
"message": "Название сборки"
},
@@ -171,7 +168,7 @@
"message": "Название сборки"
},
"app.export-modal.select-files-label": {
"message": "Выберите файлы и папки для экспорта"
"message": "Выберите файлы для экспорта"
},
"app.export-modal.version-number-label": {
"message": "Номер версии"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "Обновите {name} до последней версии, чтобы запустить игру."
},
"app.project.install-button.already-installed": {
"message": "Этот проект уже установлен"
},
"app.project.install-context.back-to-browse": {
"message": "Вернуться к поиску"
},
"app.project.install-context.install-content-to-instance": {
"message": "Установка контента в сборку"
},
"app.settings.developer-mode-enabled": {
"message": "Режим разработчика включён."
},
@@ -692,6 +698,24 @@
"instance.worlds.world_in_use": {
"message": "Мир используется"
},
"minecraft-account.add-account": {
"message": "Добавить"
},
"minecraft-account.label": {
"message": "Учётная запись Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Вход не выполнен"
},
"minecraft-account.remove-account": {
"message": "Удалить"
},
"minecraft-account.select-account": {
"message": "Выберите профиль"
},
"minecraft-account.sign-in": {
"message": "Войти в Minecraft"
},
"search.filter.locked.instance": {
"message": "Управляется сборкой"
},
@@ -717,7 +741,7 @@
"message": "Загрузчик управляется сервером"
},
"unknown-pack-warning-modal.body": {
"message": "Проверяются только файлы, загруженные на Modrinth, в том числе файлы .mrpack."
"message": "Файлы любого формата проверены на безопасность, если они загружены на Modrinth (в том числе файлы .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Больше не предупреждать"
@@ -5,12 +5,6 @@
"app.auth-servers.unreachable.header": {
"message": "Serveri za autentifikaciju su nedostupni"
},
"app.browse.add-server-to-instance": {
"message": "Dodaj server na instancu"
},
"app.browse.add-servers-to-instance": {
"message": "Dodaj servere na tvoju instancu"
},
"app.browse.add-to-instance": {
"message": "Dodaj na instancu"
},
@@ -29,9 +23,6 @@
"app.browse.discover-servers": {
"message": "Otkrij servere"
},
"app.browse.install-content-to-instance": {
"message": "Instaliraj sadržaj na instancu"
},
"app.export-modal.description-placeholder": {
"message": "Ukucaj opis modpacka..."
},
@@ -47,9 +38,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Ime modpacka"
},
"app.export-modal.select-files-label": {
"message": "Izaberi fajlove i foldere da uključiš u pack"
},
"app.export-modal.version-number-label": {
"message": "Broj verzije"
},
+57 -18
View File
@@ -1,16 +1,67 @@
{
"app.action-bar.downloading-java": {
"message": "Laddar ner Java {version}"
},
"app.action-bar.downloads": {
"message": "Nedladdningar"
},
"app.action-bar.hide-more-running-instances": {
"message": "Göm fler pågående instanser"
},
"app.action-bar.make-primary-instance": {
"message": "Gör till primärinstans"
},
"app.action-bar.no-instances-running": {
"message": "Ingen pågående instans"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primärinstans"
},
"app.action-bar.show-more-running-instances": {
"message": "Visa fler pågående instanser"
},
"app.action-bar.stop-instance": {
"message": "Avsluta instans"
},
"app.action-bar.view-active-downloads": {
"message": "Visa aktiva nedladdningar"
},
"app.action-bar.view-instance": {
"message": "Visa instans"
},
"app.action-bar.view-logs": {
"message": "Visa loggar"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Aktiverar avancerad rendering som till exempel oskärpa vilket kan leda till prestandaproblem utan hårdvaruaccelererad rendering."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Avancerad rendering"
},
"app.appearance-settings.color-theme.description": {
"message": "Välj färgtema för Modrinth-appen."
},
"app.appearance-settings.color-theme.title": {
"message": "Färgtema"
},
"app.appearance-settings.default-landing-page.home": {
"message": "Hem"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Bibliotek"
},
"app.appearance-settings.hide-nametag.title": {
"message": "Dölj namnskylt"
},
"app.auth-servers.unreachable.body": {
"message": "Minecrafts autentiseringsservrar kan vara nere just nu. Kontrollera din internetanslutning och försök igen senare."
},
"app.auth-servers.unreachable.header": {
"message": "Kan ej nå autentiseringsservrarna"
},
"app.browse.add-server-to-instance": {
"message": "Lägg till server till instans"
},
"app.browse.add-servers-to-instance": {
"message": "Lägg till servrar till din instans"
},
"app.browse.add-to-instance": {
"message": "Lägg till i instans"
},
@@ -29,18 +80,9 @@
"app.browse.discover-servers": {
"message": "Upptäck servrar"
},
"app.browse.install-content-to-instance": {
"message": "Installera innehåll till instans"
},
"app.browse.project-type.modpacks": {
"message": "Modpaket"
},
"app.browse.server.install": {
"message": "Installera"
},
"app.browse.server.installed": {
"message": "Installerad"
},
"app.browse.server.installing": {
"message": "Installerar"
},
@@ -59,9 +101,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketets namn"
},
"app.export-modal.select-files-label": {
"message": "Välj filer och mappar att inkludera i paketet"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
},
+291 -78
View File
@@ -1,18 +1,111 @@
{
"app.action-bar.downloading-java": {
"message": "ดาวน์โหลดเวอร์ชัน Java {version}"
},
"app.action-bar.downloads": {
"message": "ดาวน์โหลด"
},
"app.action-bar.hide-more-running-instances": {
"message": "ซ่อนการดำเนินการโปรแกรมเบื้องหลัง"
},
"app.action-bar.make-primary-instance": {
"message": "กำหนดโปรแกรมหลัก"
},
"app.action-bar.no-instances-running": {
"message": "ไม่มีโปรแกรมใดดำเนินการ"
},
"app.action-bar.offline": {
"message": "ออฟไลน์"
},
"app.action-bar.primary-instance": {
"message": "โปรแกรมหลัก"
},
"app.action-bar.show-more-running-instances": {
"message": "แสดงการดำเนินการโปรแกรมเบื้องหลังเพิ่มเติม"
},
"app.action-bar.stop-instance": {
"message": "หยุดการทำงานโปรแกรม"
},
"app.action-bar.view-active-downloads": {
"message": "ดูยอดการดาวน์โหลดที่ยังใช้งาน"
},
"app.action-bar.view-instance": {
"message": "ดูโปรแกรม"
},
"app.action-bar.view-logs": {
"message": "ดู Log"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "เปิดการใช้งานการเรนเดอร์ขั้นสูง เช่น เอฟเฟคการเบลอ อาจทำให้เกมเกิดปัญหาด้านประสิทธิภาพได้หากไม่มีอุปกรณ์ฮาร์ดแวร์ที่สามารถรองรับการเรนเดอร์ได้"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "การเรนเดอร์ขั้นสูง"
},
"app.appearance-settings.color-theme.description": {
"message": "เลือกธีมสีที่คุณชื่นชอบสำหรับ Modrinth บนอุปกรณ์นี้"
},
"app.appearance-settings.color-theme.title": {
"message": "ธีมสี"
},
"app.appearance-settings.default-landing-page.description": {
"message": "เปลี่ยนหน้าไปหน้าต่างที่ลันเชอร์เปิดขึ้นมา"
},
"app.appearance-settings.default-landing-page.home": {
"message": "หน้าหลัก"
},
"app.appearance-settings.default-landing-page.library": {
"message": "รายการ"
},
"app.appearance-settings.default-landing-page.title": {
"message": "หน้าเริ่มต้น"
},
"app.appearance-settings.hide-nametag.description": {
"message": "ปิดป้ายชื่อบนศีรษะของผู้เล่นในหน้าแสดงสกิน"
},
"app.appearance-settings.hide-nametag.title": {
"message": "ซ่อนป้ายชื่อ"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "การแสดงผลโลกล่าสุดที่ผู้เล่นเล่นในส่วน \"กระโดดกลับเข้าไป\" ในหน้าหลัก"
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "กระโดดกลับเข้าไปในโลก"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "ย่อลันเชอร์เมื่อกระบวนการการรัน Minecraft เริ่มต้นแล้ว"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "ย่อลันเชอร์"
},
"app.appearance-settings.native-decorations.description": {
"message": "ใช้ระบบกรอบหน้าต่าง (ต้องการการรีสตาร์ทโปรแกรมใหม่)"
},
"app.appearance-settings.native-decorations.title": {
"message": "ของตกแต่งดั้งเดิม"
},
"app.appearance-settings.select-option": {
"message": "เลือกตัวเลิือก"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "เปิดใช้งานตัวเลือกในการเลือกย่อหรือขยายแถบด้านข้าง"
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "ย่อแถบด้านข้าง"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "หากคุณกำลังติดตั้งไฟล์แพ็กของ Modrinth (.mrpack) ที่ไม่ได้โฮสต์โดยตรงบน Modrinth เราจะแจ้งให้คุณตรวจสอบให้มั่นใจก่อนที่จะติดตั้งไฟล์แพ็กดังกล่าว"
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "เตือนฉันก่อนติดตั้งแพ็กม็อดที่ไม่รู้จัก"
},
"app.auth-servers.unreachable.body": {
"message": "เซิร์ฟเวอร์ตรวจสอบสิทธิ์ของ Minecraft อาจใช้งานไม่ได้ในขณะนี้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณและลองใหม่อีกครั้งในภายหลัง"
},
"app.auth-servers.unreachable.header": {
"message": "ไม่สามารถเชื่อมต่อถึงเซิร์ฟเวอร์ได้"
},
"app.browse.add-server-to-instance": {
"message": "เพิ่มเซิร์ฟเวอร์ลงในอินสแตนซ์"
},
"app.browse.add-servers-to-instance": {
"message": "เพิ่มเซิร์ฟเวอร์ลงในอินสแตนซ์ของคุณ"
},
"app.browse.add-to-instance": {
"message": "เพิ่มลงในอินสแตนซ์"
"message": "เพิ่มลงในโปรแกรม"
},
"app.browse.add-to-instance-name": {
"message": "เพิ่มลงใน {instanceName}"
@@ -23,44 +116,77 @@
"app.browse.already-added": {
"message": "เพิ่มแล้ว"
},
"app.browse.install-content-to-instance": {
"message": "ติดตั้งเนื้อหาลงในอินสแตนซ์"
"app.browse.discover-content": {
"message": "สำรวจเนื้อหา"
},
"app.browse.discover-servers": {
"message": "สำรวจเซิร์ฟเวอร์"
},
"app.browse.hide-added-servers": {
"message": "ซ่อนเซิร์ฟเวอร์ที่ดำเนินการเพิ่มแล้ว"
},
"app.browse.project-type.modpacks": {
"message": "แพ็กม็อด"
},
"app.browse.server.installing": {
"message": "กำลังติดตั้ง"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "กำลังติดตั้งแพ็กม็อด..."
},
"app.export-modal.description-placeholder": {
"message": "กรอกคำอธิบายมอดแพ็ก.."
},
"app.export-modal.export-button": {
"message": "ส่งออก"
},
"app.export-modal.header": {
"message": "ส่งออกแพ็กม็อด"
},
"app.export-modal.include-file-accessibility-label": {
"message": "รวมถึง \"{file}\" ใช่หรือไม่"
},
"app.export-modal.modpack-name-label": {
"message": "ชื่อมอดแพ็ก"
"message": "ชื่อแพ็กม็อด"
},
"app.export-modal.modpack-name-placeholder": {
"message": "ชื่อ ม็อดแพ็ก"
"message": "ชื่อแพ็กม็อด"
},
"app.export-modal.select-files-label": {
"message": "กำหนดไฟล์ที่ต้องการจะส่งออก"
},
"app.export-modal.version-number-label": {
"message": "หมายเลขเวอร์ชัน"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "ข้อมูลทั้งหมดในอินสแตนซ์ของคุณจะถูกลบอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาที่ติดตั้งไว้ทั้งหมด"
"message": "ข้อมูลทั้งหมดที่ถูกเก็บไว้ในโปรแกรมจะถูกลบออกอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาอื่นที่ติดตั้งไว้ทั้งหมด"
},
"app.instance.confirm-delete.admonition-header": {
"message": "การดำเนินการนี้ไม่สามารถย้อนคืนได้"
},
"app.instance.confirm-delete.delete-button": {
"message": "ลบอินสแตนซ์"
"message": "ลบโปรแกรม"
},
"app.instance.confirm-delete.header": {
"message": "ลบอินสแตนซ์"
"message": "ลบโปรแกรม"
},
"app.instance.modpack-already-installed.body": {
"message": "มอดแพ็กนี้ถูกติดตั้งไว้ในอินสแตนซ์ <bold>{instanceName}</bold> อยู่แล้ว คุณแน่ใจหรือไม่ว่าต้องการสร้างสำเนาซ้ำ?"
"message": "แพ็กม็อดดังกล่าวติดตั้งไว้ใน <bold>{instanceName}</bold> อยู่แล้ว คุณแน่ใจหรือไม่ว่าต้องการจะทำซ้ำ?"
},
"app.instance.modpack-already-installed.create": {
"message": "สร้าง"
},
"app.instance.modpack-already-installed.header": {
"message": "ติดตั้งมอดแพ็กนี้อยู่แล้ว"
"message": "แพ็กม็อดดังกล่าวถูกติดตั้งไปแล้ว"
},
"app.instance.modpack-already-installed.instance": {
"message": "อินสแตนซ์"
"message": "โปรแกรม"
},
"app.instance.mods.content-type-project": {
"message": "โปรเจกต์"
@@ -72,7 +198,10 @@
"message": "เพิ่ม {count} โปรเจกต์เรียบร้อยแล้ว"
},
"app.instance.mods.share-text": {
"message": "มาตรวจดูโปรเจกต์ต่าง ๆ ที่ฉันใช้ในมอดแพ็กนี้กัน!"
"message": "มาตรวจดูโปรเจกต์ต่าง ๆ ที่ฉันใช้ในแพ็กม็อดนี้กัน!"
},
"app.instance.mods.share-title": {
"message": "แบ่งปันเนื้อหาแพ็กม็อด"
},
"app.instance.mods.successfully-uploaded": {
"message": "อัปโหลดสำเร็จ"
@@ -81,16 +210,16 @@
"message": "เพิ่มเซิร์ฟเวอร์"
},
"app.instance.worlds.browse-servers": {
"message": "ค้นหาเซิฟเวอร์"
"message": "ค้นหาเซิร์ฟเวอร์"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' จะถูกลบอย่างถาวร** และจะไม่สามารถกู้คืนได้**"
"message": "'{name}' จะ**ถูกลบออกอย่างถาวร** และจะไม่มีทางสามารถกู้คืนได้"
},
"app.instance.worlds.delete-world-title": {
"message": "คุณแน่ใจหรือไม่ว่าต้องการลบโลกนี้อย่างถาวร?"
},
"app.instance.worlds.filter-modded": {
"message": "แบบมีมอด"
"message": "มอด"
},
"app.instance.worlds.filter-offline": {
"message": "ออฟไลน์"
@@ -99,31 +228,31 @@
"message": "ออนไลน์"
},
"app.instance.worlds.filter-vanilla": {
"message": "ต้นฉบับ"
"message": "วานิลลา"
},
"app.instance.worlds.no-worlds-description": {
"message": "เพิ่มเซิร์ฟเวอร์หรือสำรวจเพื่อเริ่มต้น"
"message": "เพิ่มเซิร์ฟเวอร์หรือค้นหาเพื่อเริ่มต้น"
},
"app.instance.worlds.no-worlds-heading": {
"message": "ไม่มีเซิร์ฟเวอร์หรือโลกที่เพิ่มไว้"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' จะถูกลบออกจากรายชื่อของคุณ รวมถึงในเกมด้วย และจะไม่สามารถกู้คืนได้"
"message": "'{name}' จะถูกลบออกจากรายการของคุณ ซึ่งจะรวมถึงในเกม และการลบดังกล่าวจะไม่มีทางกู้คืนได้อีก"
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) จะถูกลบออกจากรายชื่อของคุณ รวมถึงในเกมด้วย และจะไม่สามารถกู้คืนได้"
"message": "'{name}' ({address}) จะถูกลบออกจากรายการของคุณ ซึ่งจะรวมถึงในเกม และการลบดังกล่าวจะไม่มีทางกู้คืนได้อีก"
},
"app.instance.worlds.remove-server-title": {
"message": "คุณแน่ใจหรือไม่ว่าต้องการลบ {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "ค้หา {count} โลก"
"message": "ค้หาโลกทั้งหมด {count} โลก"
},
"app.instance.worlds.this-server": {
"message": "เซิร์ฟเวอร์อันนี้"
"message": "เซิร์ฟเวอร์นี้"
},
"app.modal.install-to-play.content-required": {
"message": "จำเป็นต้องมีเนื้อหา"
"message": "เนื้อหาที่จำเป็น"
},
"app.modal.install-to-play.header": {
"message": "ติดตั้งเพื่อเล่น"
@@ -131,17 +260,20 @@
"app.modal.install-to-play.install-button": {
"message": "ติดตั้ง"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, other {# ม็อด}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "มอดแพ็กที่จำเป็น"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "เซิร์ฟเวอร์นี้จำเป็นต้องใช้มอดเพื่อเข้าเล่น คลิกติดตั้ง เพื่อตั้งค่าไฟล์ที่จำเป็นจาก Modrinth จากนั้นึงจะเข้าสู่เซิร์ฟเวอร์โดยตรงได้"
"message": "เซิร์ฟเวอร์ดังกล่าวจำเป็นต้องใช้มอดเพื่อเล่น โปรดติดตั้งและตั้งค่าไฟล์อื่นใดที่จำเป็นจาก Modrinth ก่อน จากนั้นึงจะสามารถเข้าเล่นเซิร์ฟเวอร์ได้"
},
"app.modal.install-to-play.shared-instance": {
"message": "อินสแตนซ์ที่ใช้ร่วมกัน"
"message": "โปรแกรมที่มีร่วมกัน"
},
"app.modal.install-to-play.shared-server-instance": {
"message": "อินสแตนซ์เซิร์ฟเวอร์ที่ใช้ร่วมกัน"
"message": "เซิร์ฟเวอร์ของโปรแกรมที่มีร่วมกัน"
},
"app.modal.install-to-play.view-contents": {
"message": "ดูเนื้อหา"
@@ -159,13 +291,13 @@
"message": "โหมดนักพัฒนาเปิดอยู่"
},
"app.settings.downloading": {
"message": "ดาวน์โหลด เวอร์ชัน{version}"
"message": "ดาวน์โหลดเวอร์ชัน {version}"
},
"app.settings.tabs.appearance": {
"message": "หน้าตา"
},
"app.settings.tabs.default-instance-options": {
"message": "ตัวเลือกอินสแตนซ์เริ่มต้น"
"message": "ตั้งค่าโปรแกรมเริ่มต้น"
},
"app.settings.tabs.java-installations": {
"message": "การจัดการ Java ที่ติดตั้ง"
@@ -180,22 +312,28 @@
"message": "การจัดการทรัพยากร"
},
"app.update-popup.body": {
"message": "แอฟ Modrinth v{version} พร้อมสำหรับการติดตั้งแล้ว! รีโหลดแอปเพื่ออัปเดต หรือ อัปเดตอัตโนมัติเมื่อคุณปิดแอฟ Modrinth"
"message": "Modrinth v{version} พร้อมสำหรับการติดตั้งแล้ว! เปิดโปรแกรมใหม่อีกครั้งเพื่ออัปเดตตอนนี้ หรือจะรออัปเดตอัตโนมัติ ซึ่งจะเกิดขึ้นเมื่อคุณกดปิดโปรแกรม Modrinth"
},
"app.update-popup.body.download-complete": {
"message": "แอฟ Modrinth v{version} ดาวน์โหลดเสร็จแล้ว รีโหลดแอปเพื่ออัปเดต หรือ อัปเดตอัตโนมัติเมื่อคุณปิดแอฟ Modrinth"
"message": "ดาวน์โหลด Modrinth v{version} สำเร็จแล้ว เปิดโปรแกรมใหม่อีกครั้งเพื่ออัปเดตตอนนี้ หรือจะรออัปเดตอัตโนมัติ ซึ่งจะเกิดขึ้นเมื่อคุณกดปิดโปรแกรม Modrinth"
},
"app.update-popup.body.linux": {
"message": "Modrinth App v{version} พร้อมให้อัปเดตแล้ว โปรดใช้ตัวจัดการแพ็กเกจ ของคุณเพื่ออัปเดตฟีเจอร์ใหม่และตัวแก้ไข่าสุด!"
"message": "Modrinth v{version} พร้อมให้อัปเดตแล้ว โปรดไปที่การจัดการแพ็กเกจของคุณสำหรับการอัปเดตคุณสมบัติใหม่และการแก้ไข่าง ๆ"
},
"app.update-popup.body.metered": {
"message": "แอฟ Modrinth v{version} พร้อมแล้ว! เนื่องจากคุณเปิดโหมดจำกัดปริมาณข้อมูล เราจึงไม่ได้อัปเดตอัตโนมัติ"
"message": "Modrinth v{version} พร้อมให้บริการแล้ว! แต่เนื่องจากคุณเปิดโหมดจำกัดปริมาณการใช้ข้อมูลเครือข่าย เราจึงไม่ได้อัปเดตเนื้อหาโดยอัตโนมัติ"
},
"app.update-popup.changelog": {
"message": "บันทึกการเปลี่ยนแปลง"
},
"app.update-popup.download": {
"message": "ดาวโหลด ({size})"
},
"app.update-popup.download-complete": {
"message": "ดาวน์โหลดเสร็จสมบูรณ์"
"message": "การดาวน์โหลดเสร็จสมบูรณ์"
},
"app.update-popup.reload": {
"message": "โหลดใหม่"
},
"app.update-popup.title": {
"message": "มีการอัปเดตใหม่"
@@ -207,13 +345,13 @@
"message": "เวอร์ชั่น {version} ถูกติดตั้งแล้ว"
},
"app.update.download-update": {
"message": "ดาวน์โหลดอัเดต"
"message": "ดาวน์โหลดอัเดต"
},
"app.update.downloading-update": {
"message": "ดาวน์โหลดอัเดตไปแล้ว ({percent}%)"
"message": "ดาวน์โหลดอัเดตไปแล้ว ({percent}%)"
},
"app.update.reload-to-update": {
"message": "รีโหลดเพื่อติดตั้งอัเดต"
"message": "รีโหลดเพื่อติดตั้งอัเดต"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
@@ -222,7 +360,10 @@
"message": "เลือกตัวเลือก"
},
"app.world.world-item.incompatible-version": {
"message": "เวอร์ชัน {version} ไม่รองรับ"
"message": "ไม่สามารถเข้ากับเวอร์ชัน {version} ได้"
},
"app.world.world-item.not-played-yet": {
"message": "ยังไม่เคยเล่น"
},
"app.world.world-item.offline": {
"message": "ออฟไลน์"
@@ -233,6 +374,9 @@
"friends.action.add-friend": {
"message": "เพิ่มเพื่อน"
},
"friends.action.view-friend-requests": {
"message": "คำขอเป็นเพื่อน {count} {count, plural, other {คน}}"
},
"friends.add-friend.submit": {
"message": "ส่งคำขอเป็นเพื่อน"
},
@@ -240,13 +384,13 @@
"message": "กำลังเพิ่มเพื่อน"
},
"friends.add-friend.username.description": {
"message": "อาจจะแตกต่างกับชื่อใน Minecraft"
"message": "มันอาจแตกต่างจากชื่อในเกม Minecraft ของพวกเขา!"
},
"friends.add-friend.username.placeholder": {
"message": "ใส่ชื่อบน Modrinth"
"message": "กรอกชื่อผู้ใช้ Modrinth..."
},
"friends.add-friend.username.title": {
"message": "เพื่อนของคุณมีชื่อบน Modrinth ว่าอะไร"
"message": "เพื่อนของคุณมีชื่อผู้ใช้ Modrinth ว่าอะไร?"
},
"friends.add-friends-to-share": {
"message": "<link>เพิ่มเพื่อน</link> เพื่อเห็นว่าพวกเขากำลังเล่นอะไรอยู่!"
@@ -273,13 +417,13 @@
"message": "ออฟไลน์"
},
"friends.heading.online": {
"message": "ออไลน์"
"message": "ออไลน์"
},
"friends.heading.pending": {
"message": "กําลังดำเนินการ"
},
"friends.no-friends-match": {
"message": "ไม่มีเพื่อนชื่อว่า \"{query}\""
"message": "ไม่มีรายชื่อเพื่อนที่ตรงกับ \"{query}\""
},
"friends.search-friends-placeholder": {
"message": "ค้นหาเพื่อน"
@@ -321,7 +465,7 @@
"message": "โลก Minecraft"
},
"instance.edit-world.reset-icon": {
"message": "รีเซ็ตรูปตัวอย่าง"
"message": "รีเซ็ตรูปสัญลักษณ์"
},
"instance.edit-world.title": {
"message": "แก้ไขโลก"
@@ -348,13 +492,13 @@
"message": "ทั่วไป"
},
"instance.settings.tabs.general.delete": {
"message": "ลบอินสแตนซ์"
"message": "ลบโปรแกรม"
},
"instance.settings.tabs.general.delete.button": {
"message": "ลบอินสแตนซ์"
"message": "ลบโปรแกรม"
},
"instance.settings.tabs.general.delete.description": {
"message": "ลบอินสแตนซ์ออกจากอุปกรณ์ของคุณอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาทั้งหมดที่ติดตั้งไว้ โปรดระมัดระวัง เนื่องจากเมื่อถูกลบแล้วจะไม่สามารถกู้คืนได้อีก"
"message": "การกระทำดังกล่าวจะลบโปรแกรมออกจากอุปกรณ์ของคุณอย่างถาวร รวมถึงโลก การตั้งค่า และเนื้อหาทั้งหมดที่ติดตั้งไว้ โปรดระมัดระวัง เนื่องจากเมื่อถูกลบแล้วจะไม่สามารถกู้คืนได้อีก"
},
"instance.settings.tabs.general.deleting.button": {
"message": "กำลังลบ..."
@@ -366,31 +510,31 @@
"message": "ไม่สามารถทำซ้ำได้ขณะติดตั้ง"
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "สร้างสำเนาอินสแตนซ์"
"message": "สร้างสำเนาโปรแกรม"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "สร้างสำเนาของอินสแตนซ์นี้ รวมถึงโลก การตั้งค่า ม็อด และอื่นๆ"
"message": "สร้างสำเนาของโปรแกรมนี้ รวมถึงโลก การตั้งค่า ม็อด และอื่น ๆ"
},
"instance.settings.tabs.general.edit-icon": {
"message": "แก้ไขไอคอน"
"message": "แก้ไขสัญลักษณ์"
},
"instance.settings.tabs.general.edit-icon.remove": {
"message": "ลบไอคอน"
"message": "ลบสัญลักษณ์"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "เปลี่ยนไอคอน"
"message": "แทนที่สัญลักษณ์"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "เลือกรูปไอคอน"
"message": "เลือกรูปสัญลักษณ์"
},
"instance.settings.tabs.general.library-groups": {
"message": "การจัดกลุ่ม"
"message": "จัดกลุ่ม"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "สร้างกลุ่มใหม่"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "กลุ่มไลบรารีช่วยให้คุณจัดระเบียบอินสแตนซ์ของคุณเป็นหมวดหมู่ต่างๆ ภายในไลบรารี"
"message": "การจัดกลุ่มจะช่วยจัดระเบียบโปรแกรมของคุณภายในหน้ารายการให้เป็นหมวดหมู่"
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "ใส่ชื่อกลุ่ม"
@@ -399,43 +543,58 @@
"message": "ชื่อ"
},
"instance.settings.tabs.hooks": {
"message": "ตัวดักการปล่อย"
"message": "Launch Hook"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "กำหนดเอง"
"message": "กำหนด Launch Hook เอง"
},
"instance.settings.tabs.hooks.description": {
"message": "ตัวดักจับช่วยให้ผู้ใช้ระดับสูงสามารถรันคำสั่งระบบบางอย่างได้ ทั้งก่อนและหลังการเปิดเกม"
"message": "Launch Hook จะช่วยให้ผู้เล่นที่ชำนาญสามารถใช้คำสั่งต่อระบบบางอย่างได้ ทั้งก่อนและหลังเกมถูกเปิด"
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Post-exit"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "จะรันหลังจากที่ตัวเกมถูกปิด"
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "ป้อนคำสั่งหลังจบการทำงาน..."
"message": "ป้อนคำสั่ง..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "ก่อนรัน"
"message": "Pre-launch"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "จะรันก่อนที่โปรแกรมจะทำงาน"
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "ป้อนคำสั่ง..."
},
"instance.settings.tabs.hooks.title": {
"message": "ฮุกการเปิดเกม"
"message": "Game Launch Hook"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "ตัวห่อหุ้ม"
"message": "Wrapper"
},
"instance.settings.tabs.hooks.wrapper.description": {
"message": "คำสั่งตัวห่อหุ้มสำหรับเปิด ไมน์คราฟต์"
"message": "คำสั่ง Wrapper สำหรับใช้ในการรันเปิด Minecraft"
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "ป้อนตัวห่อหุ้มคำสั่ง..."
"message": "ป้อนคำสั่ง Wrapper..."
},
"instance.settings.tabs.installation": {
"message": "การติดตั้ง"
},
"instance.settings.tabs.installation.loader-version": {
"message": "เวอร์ชัน {loader}"
"message": "เวอร์ชัน {loader}"
},
"instance.settings.tabs.java": {
"message": "Java และ หน่วยความจำ"
},
"instance.settings.tabs.java.environment-variables": {
"message": "Environment Variables"
},
"instance.settings.tabs.java.hooks": {
"message": "ฮุก"
"message": "Hook"
},
"instance.settings.tabs.java.java-arguments": {
"message": "อาร์กิวเมนต์ Java"
@@ -449,6 +608,9 @@
"instance.settings.tabs.window": {
"message": "หน้าต่าง"
},
"instance.settings.tabs.window.custom-window-settings": {
"message": "การตั้งค่าการปรับแต่งหน้าต่าง"
},
"instance.settings.tabs.window.fullscreen": {
"message": "เต็มหน้าจอ"
},
@@ -474,7 +636,7 @@
"message": "ป้อนความกว้าง..."
},
"instance.worlds.a_minecraft_server": {
"message": "เซิร์ฟเวอร์ ไมน์คราฟต์"
"message": "เซิร์ฟเวอร์ Minecraft"
},
"instance.worlds.cant_connect": {
"message": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้"
@@ -486,7 +648,7 @@
"message": "ไม่ต้องแสดงในหน้าแรก"
},
"instance.worlds.game_already_open": {
"message": "อินสแตนซ์ถูกเปิดอยู่แล้ว"
"message": "โปรแกรมถูกเปิดอยู่แล้ว"
},
"instance.worlds.hardcore": {
"message": "โหมดฮาร์ดคอร์"
@@ -507,24 +669,75 @@
"message": "คุณสามารถเข้าสู่โลกผู้เล่นคนเดียวได้โดยตรงบน Minecraft 1.20 ขึ้นไปเท่านั้น"
},
"instance.worlds.play_instance": {
"message": "เริ่มเล่นอินสแตนซ์"
"message": "เริ่มรันโปรแกรม"
},
"instance.worlds.view_instance": {
"message": "ดูอินสแตนซ์"
"message": "ดูโปรแกรม"
},
"instance.worlds.world_in_use": {
"message": "โลกนี้กำลังถูกใช้งานอยู่"
},
"minecraft-account.add-account": {
"message": "เพิ่มบัญชี"
},
"minecraft-account.label": {
"message": "บัญชี Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "ยังไม่ได้ลงชื่อเข้าใช้"
},
"minecraft-account.remove-account": {
"message": "ลบบัญชี"
},
"minecraft-account.select-account": {
"message": "เลือกบัญชี"
},
"minecraft-account.sign-in": {
"message": "ลงชื่อเข้าใช้ Minecraft"
},
"search.filter.locked.instance": {
"message": "อินสแตนซ์เป็นผู้จัดเตรียม"
"message": "โปรแกรมเป็นผู้กำหนด"
},
"search.filter.locked.instance-game-version.title": {
"message": "เวอร์ชันเกมถูกกำหนดไว้โดยโปรแกรมแล้ว"
},
"search.filter.locked.instance-loader.title": {
"message": "ตัวรันถูกกำหนดไว้โดยโปรแกรมแล้ว"
},
"search.filter.locked.instance.sync": {
"message": "ซิงค์กับอินสแตนซ์"
"message": "เชื่อมต่อกับโปรแกรม"
},
"search.filter.locked.server": {
"message": "เซิร์ฟเวอร์เป็นผู้จัดเตรียม"
"message": "เซิร์ฟเวอร์เป็นผู้กำหนด"
},
"search.filter.locked.server-environment.title": {
"message": "มีเพียงม็อดสำหรับฝั่งเครื่องของผู้เล่นเท่านั้นที่สามารถเพิ่มลงในโปรแกรมเซิร์ฟเวอร์ดังกล่าวได้"
},
"search.filter.locked.server-game-version.title": {
"message": "เซิร์ฟเวอร์เป็นผู้จัดเตรียมเวอร์ชันเกม"
"message": "เวอร์ชันเกมถูกกำหนดโดยเซิร์ฟเวอร์แล้ว"
},
"search.filter.locked.server-loader.title": {
"message": "ตัวรันถูกกำหนดโดยเซิร์ฟเวอร์แล้ว"
},
"unknown-pack-warning-modal.body": {
"message": "ไฟล์จะได้รับการตรวจสอบโดยไม่คำนึงถึงประเภทของไฟล์ (รวมทั้ง .mrpack) เมื่อไฟล์ถูกอัปโหลดขึ้น Modrinth"
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "อย่าแสดงคำเตือนนี้อีก"
},
"unknown-pack-warning-modal.header": {
"message": "ยืนยันการติดตั้ง"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "ดำเนินการติดตั้งต่อไป"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "มัลแวร์มักแฝงตัวมากับไฟล์แพ็กม็อดผ่านการแชร์ผ่านแพลตฟอร์มที่ไม่ใช่แพลตฟอร์มเฉพาะ เช่น ดิสคอร์ด"
},
"unknown-pack-warning-modal.warning.body": {
"message": "เราไม่สามารถค้นหาไฟล์ดังกล่าวได้บน Modrinth พวกเราขอแนะนำอย่างมากกว่าควรติดตั้งไฟล์จากแหล่งที่น่าเชื่อถือเท่านั้น"
},
"unknown-pack-warning-modal.warning.title": {
"message": "แจ้งเตือนไฟล์ไม่รู้จัก"
}
}
+18 -21
View File
@@ -104,12 +104,6 @@
"app.auth-servers.unreachable.header": {
"message": "Doğrulama sunucularına erişilemedi"
},
"app.browse.add-server-to-instance": {
"message": "Sunucuyu kurulumu ekle"
},
"app.browse.add-servers-to-instance": {
"message": "Kurulumunuza sunucular ekleyin"
},
"app.browse.add-to-instance": {
"message": "Kuruluma ekle"
},
@@ -131,21 +125,9 @@
"app.browse.hide-added-servers": {
"message": "Zaten eklenmiş sunucuları gizle"
},
"app.browse.hide-installed-content": {
"message": "Zaten yüklenmiş içerikleri gizle"
},
"app.browse.install-content-to-instance": {
"message": "İçeriği kuruluma yükle"
},
"app.browse.project-type.modpacks": {
"message": "Mod paketleri"
},
"app.browse.server.install": {
"message": "Yükle"
},
"app.browse.server.installed": {
"message": "Yüklü"
},
"app.browse.server.installing": {
"message": "Yükleniyor"
},
@@ -170,9 +152,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modpaketi adı"
},
"app.export-modal.select-files-label": {
"message": "Pakete dahil edilecek dosya ve klasörleri seçin"
},
"app.export-modal.version-number-label": {
"message": "Sürüm numarası"
},
@@ -692,6 +671,24 @@
"instance.worlds.world_in_use": {
"message": "Dünya şu anda kullanımda"
},
"minecraft-account.add-account": {
"message": "Hesap ekle"
},
"minecraft-account.label": {
"message": "Minecraft hesabı"
},
"minecraft-account.not-signed-in": {
"message": "Giriş yapılmadı"
},
"minecraft-account.remove-account": {
"message": "Hesabı kaldır"
},
"minecraft-account.select-account": {
"message": "Hesap seçin"
},
"minecraft-account.sign-in": {
"message": "Minecraft'a giriş yapın"
},
"search.filter.locked.instance": {
"message": "Kurulum tarafından sağlanan"
},
+76 -52
View File
@@ -6,49 +6,49 @@
"message": "Завантаження"
},
"app.action-bar.hide-more-running-instances": {
"message": "Приховати більше запущених екземплярів"
"message": "Приховати більше запущених профілів"
},
"app.action-bar.make-primary-instance": {
"message": "Зробити основним екземпляром"
"message": "Зробити основним профілем"
},
"app.action-bar.no-instances-running": {
"message": "Немає запущених екземплярів"
"message": "Немає запущених профілів"
},
"app.action-bar.offline": {
"message": "Офлайн"
"message": "Поза мережею"
},
"app.action-bar.primary-instance": {
"message": "Основний екземпляр"
"message": "Основний профіль"
},
"app.action-bar.show-more-running-instances": {
"message": "Показати більше запущених екземплярів"
"message": "Показати більше запущених профілів"
},
"app.action-bar.stop-instance": {
"message": "Зупинити екземпляр"
"message": "Зупинити профіль"
},
"app.action-bar.view-active-downloads": {
"message": "Переглянути активні завантаження"
},
"app.action-bar.view-instance": {
"message": "Переглянути екземпляр"
"message": "Переглянути профіль"
},
"app.action-bar.view-logs": {
"message": "Переглянути журнали"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Дозволяє розширений рендеринг, такий як ефекти розмиття, які можуть спричиняти проблеми з продуктивністю без апаратно-прискореного рендерингу."
"message": "Дозволяє розширене промальовування, як-от ефекти розмиття, які можуть спричиняти проблеми з продуктивністю без апаратно-прискореного промальовування."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Розширений рендеринг"
"message": "Розширене промальовування"
},
"app.appearance-settings.color-theme.description": {
"message": "Виберіть бажану колірну тему для програми Modrinth App."
"message": "Виберіть бажану колірну тему для Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Колірна тема"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Змініть сторінку, на якій відкривається панель запуску."
"message": "Змініть сторінку, на якій відкривається запускач."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Головна"
@@ -57,46 +57,46 @@
"message": "Бібліотека"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Цільова сторінка за замовчуванням"
"message": "Усталена цільова сторінка"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Вимикає іменний бейдж над вашим гравцем на сторінці скінів."
"message": "Вимикає ім над вашим гравцем на сторінці скінів."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Приховати бейдж з іменем"
"message": "Приховати ім'я"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Включає нещодавні світи в розділі «Повернення» на головній сторінці."
"message": "Уключає нещодавні світи в розділі «Повернутися» на головній сторінці."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Стрибни назад у світи"
"message": "Повернутися у світи"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Згорнути лаунчер під час запуску процесу Minecraft."
"message": "Згортає запускач під час запуску процесу Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Згорнути панель запуску"
"message": "Мінімізувати запускач"
},
"app.appearance-settings.native-decorations.description": {
"message": "Використовувати рамку системного вікна (потрібно перезапустити програму)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Рідні прикраси"
"message": "Системне оформлення"
},
"app.appearance-settings.select-option": {
"message": "Виберіть опцію"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Вмикає можливість перемикання бічної панелі."
"message": "Умикає можливість перемикання бокової панелі."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Перемикання бічної панелі"
"message": "Перемикання бокової панелі"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж встановлювати його."
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж установлювати його."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Попереджати мене перед встановленням невідомих модпаків"
"message": "Попереджати мене перед установленням невідомих збірок"
},
"app.auth-servers.unreachable.body": {
"message": "Сервери автентифікації Minecraft можуть зараз не працювати. Перевірте з’єднання з інтернетом та спробуйте пізніше."
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "Не вдається зв’язатися зі серверами автентифікації"
},
"app.browse.add-server-to-instance": {
"message": "Додати сервер до профілю"
},
"app.browse.add-servers-to-instance": {
"message": "Додати сервера до вашого профілю"
"message": "Додавання сервера до профілю"
},
"app.browse.add-to-an-instance": {
"message": "Додати до профілю"
},
"app.browse.add-to-instance": {
"message": "Додати до профілю"
@@ -122,6 +122,9 @@
"app.browse.already-added": {
"message": "Уже додано"
},
"app.browse.back-to-instance": {
"message": "Назад до профілю"
},
"app.browse.discover-content": {
"message": "Дослідити вміст"
},
@@ -129,34 +132,25 @@
"message": "Дослідити сервера"
},
"app.browse.hide-added-servers": {
"message": "Скрити наявні сервери"
},
"app.browse.hide-installed-content": {
"message": "Скрити доданий контент"
},
"app.browse.install-content-to-instance": {
"message": "Установити вміст в профіль"
"message": "Сховати наявні сервери"
},
"app.browse.project-type.modpacks": {
"message": "Модпаки"
"message": "Збірки"
},
"app.browse.server.install": {
"message": "Установлено"
},
"app.browse.server.installed": {
"message": "Встановлено"
"app.browse.server-instance-content-warning": {
"message": "Додавання вмісту може порушити сумісність при приєднанні до сервера. Усі додані матеріали також будуть утрачені під час оновлення вмісту профілю сервера."
},
"app.browse.server.installing": {
"message": "Встановлення"
"message": "Установлення"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Встановлення модпаку..."
"message": "Установлення збірки…"
},
"app.export-modal.description-placeholder": {
"message": "Уведіть опис збірки..."
"message": "Уведіть опис збірки"
},
"app.export-modal.export-button": {
"message": "Експортувати"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "Експортувати збірку"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Уключити «{file}»?"
},
"app.export-modal.modpack-name-label": {
"message": "Назва збірки"
},
@@ -171,7 +168,7 @@
"message": "Назва збірки"
},
"app.export-modal.select-files-label": {
"message": "Виберіть файли й теки, щоб додати їх до збірки"
"message": "Змінити які файли додані до експорту"
},
"app.export-modal.version-number-label": {
"message": "Номер версії"
@@ -180,7 +177,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, конфігураціями та усім встановленим контентом."
"message": "Усі дані вашого профілю будуть видалені назавжди, включно з вашими світами, налаштуваннями та всім установленим умістом."
},
"app.instance.confirm-delete.admonition-header": {
"message": "Ця дія є незворотною"
@@ -234,7 +231,7 @@
"message": "Ви впевнені, що хочете назавжди видалити цей світ?"
},
"app.instance.worlds.filter-modded": {
"message": "Модифікований"
"message": "Модифіковано"
},
"app.instance.worlds.filter-offline": {
"message": "Поза мережею"
@@ -243,7 +240,7 @@
"message": "У мережі"
},
"app.instance.worlds.filter-vanilla": {
"message": "Ванільний"
"message": "Стандартний"
},
"app.instance.worlds.no-worlds-description": {
"message": "Додати сервер або переглянути, щоб почати"
@@ -261,7 +258,7 @@
"message": "Ви впевнені, що хочете видалити «{name}»?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Пошук {count} світів..."
"message": "Пошук {count} світів"
},
"app.instance.worlds.this-server": {
"message": "цей сервер"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "«{name}» потребує оновлення, щоб грати. Будь ласка, оновіть гру до останньої версії, щоб запустити її."
},
"app.project.install-button.already-installed": {
"message": "Проєкт уже встановлено"
},
"app.project.install-context.back-to-browse": {
"message": "Повернутися до дослідження"
},
"app.project.install-context.install-content-to-instance": {
"message": "Установити вміст до профілю"
},
"app.settings.developer-mode-enabled": {
"message": "Увімкнено режим розробника."
},
@@ -441,7 +447,7 @@
"message": "Немає друзів, які збігаються з «{query}»"
},
"friends.search-friends-placeholder": {
"message": "Пошук друзів..."
"message": "Пошук друзів"
},
"friends.section.heading": {
"message": "{title} — {count}"
@@ -489,7 +495,7 @@
"message": "Додавання файлів ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Зберегти як..."
"message": "Зберегти як"
},
"instance.server-modal.address": {
"message": "Адреса"
@@ -692,6 +698,24 @@
"instance.worlds.world_in_use": {
"message": "Світ наразі використовується"
},
"minecraft-account.add-account": {
"message": "Додати обліковий запис"
},
"minecraft-account.label": {
"message": "Майнкрафт аккаунт"
},
"minecraft-account.not-signed-in": {
"message": "Не ввійдено в систему"
},
"minecraft-account.remove-account": {
"message": "Видалити обліковий запис"
},
"minecraft-account.select-account": {
"message": "Обрати обліковий запис"
},
"minecraft-account.sign-in": {
"message": "Увійти в Майнкрафт"
},
"search.filter.locked.instance": {
"message": "Надано профілем"
},
@@ -729,7 +753,7 @@
"message": "Усе одно інсталювати"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Шкідливе програмне забезпечення часто поширюють через файли модпаків, які публікуються на таких платформах, як Discord."
"message": "Шкідливе програмне забезпечення часто поширюють через файли збірок, які публікуються на таких платформах, як Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ми не змогли знайти цей файл на Modrinth. Ми рекомендуємо встановлювати файли лише з тих джерел яким ви довіряєте."
@@ -5,12 +5,6 @@
"app.auth-servers.unreachable.header": {
"message": "Không thể kết nối đến máy chủ xác thực"
},
"app.browse.add-server-to-instance": {
"message": "Thêm máy chủ vào hồ sơ"
},
"app.browse.add-servers-to-instance": {
"message": "Thêm máy chủ vào hồ sơ của bạn"
},
"app.browse.add-to-instance": {
"message": "Thêm vào hồ sơ"
},
@@ -29,9 +23,6 @@
"app.browse.discover-servers": {
"message": "Khám phá máy chủ"
},
"app.browse.install-content-to-instance": {
"message": "Tải nội dung này vào hồ sơ"
},
"app.export-modal.description-placeholder": {
"message": "Thêm miêu tả cho gói modpack..."
},
@@ -47,9 +38,6 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Tên modpack"
},
"app.export-modal.select-files-label": {
"message": "Chọn tệp và gói để thêm vào gói tài nguyên"
},
"app.export-modal.version-number-label": {
"message": "Phiên bản"
},
+28 -22
View File
@@ -12,7 +12,7 @@
"message": "设为主实例"
},
"app.action-bar.no-instances-running": {
"message": "没有实例正在运行"
"message": "没有运行中的实例"
},
"app.action-bar.offline": {
"message": "离线"
@@ -60,10 +60,10 @@
"message": "默认起始页"
},
"app.appearance-settings.hide-nametag.description": {
"message": "在皮肤页面中禁用你玩家头顶上的名称标签。"
"message": "在皮肤页面中禁用你玩家头顶上的名。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "隐藏名称标签"
"message": "隐藏名"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "在主页的“快速回到”部分包含最近的世界。"
@@ -104,14 +104,14 @@
"app.auth-servers.unreachable.header": {
"message": "无法连接到身份验证服务器"
},
"app.browse.add-server-to-instance": {
"message": "将服务器添加到实例实例"
},
"app.browse.add-servers-to-instance": {
"message": "将服务器添加到你的实例"
"message": "将服务器添加到实例"
},
"app.browse.add-to-an-instance": {
"message": "添加到实例"
},
"app.browse.add-to-instance": {
"message": "添加到实例实例"
"message": "添加到实例"
},
"app.browse.add-to-instance-name": {
"message": "添加到 {instanceName}"
@@ -120,7 +120,10 @@
"message": "已添加"
},
"app.browse.already-added": {
"message": "已添加"
"message": "已添加"
},
"app.browse.back-to-instance": {
"message": "返回实例"
},
"app.browse.discover-content": {
"message": "发现内容"
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "隐藏已添加的服务器"
},
"app.browse.hide-installed-content": {
"message": "隐藏已安装的内容"
},
"app.browse.install-content-to-instance": {
"message": "将内容安装到实例中"
},
"app.browse.project-type.modpacks": {
"message": "整合包"
},
"app.browse.server.install": {
"message": "安装"
},
"app.browse.server.installed": {
"message": "已安装"
"app.browse.server-instance-content-warning": {
"message": "加入服务器时,添加内容可能会破坏兼容性。当更新服务器实例内容时任何添加的内容也将丢失。"
},
"app.browse.server.installing": {
"message": "安装中"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "导出整合包"
},
"app.export-modal.include-file-accessibility-label": {
"message": "包含 \"{file}\""
},
"app.export-modal.modpack-name-label": {
"message": "整合包名称"
},
@@ -171,7 +168,7 @@
"message": "整合包名称"
},
"app.export-modal.select-files-label": {
"message": "选择要包含在包中的文件和文件夹"
"message": "配置此导出中包含哪些文件"
},
"app.export-modal.version-number-label": {
"message": "版本号"
@@ -302,6 +299,15 @@
"app.modal.update-to-play.update-required-description": {
"message": "需要更新至最新版本才能运行 {name}。请更新后启动游戏。"
},
"app.project.install-button.already-installed": {
"message": "此项目已安装"
},
"app.project.install-context.back-to-browse": {
"message": "返回发现页"
},
"app.project.install-context.install-content-to-instance": {
"message": "将内容安装到实例中"
},
"app.settings.developer-mode-enabled": {
"message": "开发者模式已启用。"
},
@@ -561,7 +567,7 @@
"message": "启动 Hooks"
},
"instance.settings.tabs.hooks.custom-hooks": {
"message": "自定义启动Hooks"
"message": "自定义启动 Hooks"
},
"instance.settings.tabs.hooks.description": {
"message": "Hooks允许高级用户在启动游戏前后运行特定的系统命令。"
+27 -24
View File
@@ -104,11 +104,11 @@
"app.auth-servers.unreachable.header": {
"message": "無法連線到驗證伺服器"
},
"app.browse.add-server-to-instance": {
"message": "將伺服器新增到實例"
},
"app.browse.add-servers-to-instance": {
"message": "將伺服器新增實例"
"message": "將伺服器新增實例"
},
"app.browse.add-to-an-instance": {
"message": "新增至實例"
},
"app.browse.add-to-instance": {
"message": "新增至實例"
@@ -120,7 +120,10 @@
"message": "已新增"
},
"app.browse.already-added": {
"message": "已新增"
"message": "已新增"
},
"app.browse.back-to-instance": {
"message": "回到實例"
},
"app.browse.discover-content": {
"message": "探索內容"
@@ -131,20 +134,11 @@
"app.browse.hide-added-servers": {
"message": "隱藏已新增的伺服器"
},
"app.browse.hide-installed-content": {
"message": "隱藏已安裝的內容"
},
"app.browse.install-content-to-instance": {
"message": "安裝至實例中"
},
"app.browse.project-type.modpacks": {
"message": "模組包"
},
"app.browse.server.install": {
"message": "安裝"
},
"app.browse.server.installed": {
"message": "已安裝"
"app.browse.server-instance-content-warning": {
"message": "加入內容可能會導致加入伺服器時發生相容性問題。當你更新伺服器實例內容時,任何新增的內容也會遺失。"
},
"app.browse.server.installing": {
"message": "安裝中"
@@ -164,6 +158,9 @@
"app.export-modal.header": {
"message": "匯出模組包"
},
"app.export-modal.include-file-accessibility-label": {
"message": "要包含「{file}」嗎?"
},
"app.export-modal.modpack-name-label": {
"message": "模組包名稱"
},
@@ -171,7 +168,7 @@
"message": "模組包名稱"
},
"app.export-modal.select-files-label": {
"message": "選擇要包含在模組包中的檔案與資料夾"
"message": "設定要包含在此匯出檔案中的檔案"
},
"app.export-modal.version-number-label": {
"message": "版本號碼"
@@ -198,7 +195,7 @@
"message": "建立"
},
"app.instance.modpack-already-installed.header": {
"message": "模組包已安裝"
"message": "模組包已安裝"
},
"app.instance.modpack-already-installed.instance": {
"message": "實例"
@@ -302,6 +299,12 @@
"app.modal.update-to-play.update-required-description": {
"message": "需要更新才能遊玩「{name}」。請更新至最新版本以啟動遊戲。"
},
"app.project.install-button.already-installed": {
"message": "這個專案已安裝"
},
"app.project.install-context.install-content-to-instance": {
"message": "將內容安裝至實例"
},
"app.settings.developer-mode-enabled": {
"message": "開發人員模式已啟用。"
},
@@ -693,22 +696,22 @@
"message": "世界正在使用中"
},
"minecraft-account.add-account": {
"message": "添加賬號"
"message": "新增帳號"
},
"minecraft-account.label": {
"message": "Minecraft 號"
"message": "Minecraft 號"
},
"minecraft-account.not-signed-in": {
"message": "未登"
"message": "未登"
},
"minecraft-account.remove-account": {
"message": "移除號"
"message": "移除號"
},
"minecraft-account.select-account": {
"message": "選擇號"
"message": "選擇號"
},
"minecraft-account.sign-in": {
"message": "登 Minecraft"
"message": "登 Minecraft"
},
"search.filter.locked.instance": {
"message": "由該實例提供"
+53 -36
View File
@@ -10,6 +10,7 @@ import {
} from '@modrinth/assets'
import type { BrowseInstallContentType, CardAction, ProjectType, Tags } from '@modrinth/ui'
import {
BrowseInstallHeader,
BrowsePageLayout,
BrowseSidebar,
commonMessages,
@@ -22,8 +23,9 @@ import {
preferencesDiffer,
provideBrowseManager,
requestInstall,
SelectedProjectsFloatingBar,
useBrowseSearch,
useDebugLogger,
useStickyObserver,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
@@ -56,18 +58,25 @@ import {
} from '@/providers/setup/server-install-content'
import { useBreadcrumbs } from '@/store/breadcrumbs'
defineOptions({
name: 'Browse',
})
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
injectServerInstall()
const { install: installVersion } = injectContentInstall()
const queryClient = useQueryClient()
const debugLog = useDebugLogger('Browse')
const router = useRouter()
const route = useRoute()
const browseRouteActive = computed(() => route.path.startsWith('/browse/'))
const serverSetupModalRef = ref<InstanceType<typeof CreationFlowModal> | null>(null)
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
const serverInstallContent = createServerInstallContent({
serverSetupModalRef,
isRouteInContext: (targetRoute) => targetRoute.path.startsWith('/browse/'),
})
provideServerInstallContent(serverInstallContent)
const {
serverIdQuery,
@@ -77,6 +86,7 @@ const {
isSetupServerContext,
effectiveServerWorldId,
serverContextServerData,
serverContextWorldName,
serverContentProjectIds,
queuedServerInstallProjectIds,
queuedServerInstallCount,
@@ -103,8 +113,6 @@ const {
handleServerModpackFlowCreate,
markServerProjectInstalled,
} = serverInstallContent
debugLog('fetching tags (categories, loaders, gameVersions)')
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories()
.catch(handleError)
@@ -182,22 +190,10 @@ watchServerContextChanges()
await initInstanceContext()
async function initInstanceContext() {
debugLog('initInstanceContext', {
queryI: route.query.i,
queryAi: route.query.ai,
querySid: route.query.sid,
queryWid: route.query.wid,
queryFrom: route.query.from,
})
await initServerContext()
if (route.query.i) {
instance.value = (await getInstance(route.query.i as string).catch(handleError)) ?? null
debugLog('instance loaded', {
name: instance.value?.name,
loader: instance.value?.loader,
gameVersion: instance.value?.game_version,
})
if (route.query.from === 'worlds') {
get_profile_worlds(route.query.i as string)
@@ -205,34 +201,29 @@ async function initInstanceContext() {
const serverProjectIds = worlds
.filter((w) => w.type === 'server' && 'project_id' in w && w.project_id)
.map((w) => (w as { project_id: string }).project_id)
debugLog('installedServerProjectIds loaded', { count: serverProjectIds.length })
installedProjectIds.value = serverProjectIds
})
.catch(handleError)
} else {
getInstalledProjectIds(route.query.i as string)
.then((ids) => {
debugLog('installedProjectIds loaded', { count: ids?.length })
installedProjectIds.value = ids
})
.catch(handleError)
}
if (instance.value?.linked_data?.project_id) {
debugLog('checking linked project for server status', instance.value.linked_data.project_id)
const projectV3 = await get_project_v3(
instance.value.linked_data.project_id,
'must_revalidate',
).catch(handleError)
if (projectV3?.minecraft_server != null) {
debugLog('instance is a server instance')
isServerInstance.value = true
}
}
}
if (route.query.ai && !(route.params.projectType === 'modpack')) {
debugLog('setting instanceHideInstalled from query', route.query.ai)
instanceHideInstalled.value = route.query.ai === 'true'
}
}
@@ -283,7 +274,7 @@ function syncHiddenServerContentProjectIds() {
watch(
serverContentProjectIds,
() => {
if (!hiddenServerContentProjectIdsInitialized.value) {
if (!hiddenServerContentProjectIdsInitialized.value || serverHideInstalled.value) {
syncHiddenServerContentProjectIds()
}
},
@@ -356,11 +347,9 @@ const {
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
debugLog('went offline')
offline.value = true
})
window.addEventListener('online', () => {
debugLog('went online')
offline.value = false
})
@@ -405,6 +394,10 @@ const messages = defineMessages({
id: 'app.browse.back-to-instance',
defaultMessage: 'Back to instance',
},
worldFallbackName: {
id: 'app.browse.server.world-fallback-name',
defaultMessage: 'Instance',
},
serverInstanceContentWarning: {
id: 'app.browse.server-instance-content-warning',
defaultMessage:
@@ -464,6 +457,9 @@ const projectType = ref<ProjectType>(route.params.projectType as ProjectType)
watch(
() => route.params.projectType as ProjectType,
async (newType) => {
if (!browseRouteActive.value) {
return
}
if (isSetupServerContext.value) {
enforceSetupModpackRoute(newType)
if (newType !== 'modpack') return
@@ -471,11 +467,9 @@ watch(
if (!newType || newType === projectType.value) return
debugLog('projectType route param changed', { from: projectType.value, to: newType })
projectType.value = newType
if (!route.query.i && instance.value) {
debugLog('instance context removed, resetting')
instance.value = null
installedProjectIds.value = null
instanceHideInstalled.value = false
@@ -545,8 +539,9 @@ const selectableProjectTypes = computed(() => {
const installContext = computed(() => {
if (isServerContext.value && serverContextServerData.value) {
return {
name: serverContextServerData.value.name,
name: serverContextWorldName.value ?? formatMessage(messages.worldFallbackName),
loader: serverContextServerData.value.loader ?? '',
loaderVersion: serverContextServerData.value.loader_version ?? '',
gameVersion: serverContextServerData.value.mc_version ?? '',
serverId: serverIdQuery.value,
upstream: serverContextServerData.value.upstream,
@@ -586,6 +581,12 @@ const installContext = computed(() => {
return null
})
const stickyInstallHeaderRef = ref<HTMLElement | null>(null)
const { isStuck: isInstallHeaderStuck } = useStickyObserver(
stickyInstallHeaderRef,
'BrowseInstallHeader',
)
const installingProjectIds = ref<Set<string>>(new Set())
function setProjectInstalling(projectId: string, installing: boolean) {
@@ -682,11 +683,10 @@ function getCardActions(
installed?: boolean
installing?: boolean
}
const isInstalled =
projectResult.installed ||
allInstalledIds.value.has(projectResult.project_id || '') ||
serverContentProjectIds.value.has(projectResult.project_id || '') ||
serverContextServerData.value?.upstream?.project_id === projectResult.project_id
const isInstalled = isServerContext.value
? serverContentProjectIds.value.has(projectResult.project_id || '') ||
serverContextServerData.value?.upstream?.project_id === projectResult.project_id
: projectResult.installed || allInstalledIds.value.has(projectResult.project_id || '')
const isInstalling = installingProjectIds.value.has(projectResult.project_id)
if (
@@ -838,7 +838,6 @@ function onSearchResultInstalled(id: string) {
}
async function search(requestParams: string) {
debugLog('searching v3', requestParams)
const isServer = projectType.value === 'server'
const rawResults = await queryClient.fetchQuery({
@@ -920,6 +919,7 @@ const lockedFilterMessages = computed(() => ({
const searchState = useBrowseSearch({
projectType,
tags,
active: browseRouteActive,
providedFilters: combinedProvidedFilters,
search,
persistentQueryParams: ['i', 'ai', 'shi', 'sid', 'wid', 'from'],
@@ -967,7 +967,12 @@ if (instance.value?.game_version) {
await searchState.refreshSearch()
function getProjectBrowseQuery() {
if (!installContext.value) return undefined
if (!browseRouteActive.value) {
return undefined
}
if (!installContext.value) {
return undefined
}
return {
...route.query,
b: route.fullPath,
@@ -1031,6 +1036,17 @@ provideBrowseManager({
<template>
<div class="flex flex-col gap-3 p-6">
<div
v-if="installContext"
ref="stickyInstallHeaderRef"
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid border-divider bg-surface-1 px-6 pt-6"
:class="[isInstallHeaderStuck ? 'border-t' : '']"
>
<BrowseInstallHeader bottom-padding />
</div>
<SelectedProjectsFloatingBar v-if="installContext" />
<BrowsePageLayout>
<template #after>
<ContextMenu ref="contextMenuRef" @option-clicked="handleOptionsClick">
@@ -1057,7 +1073,8 @@ provideBrowseManager({
@browse-modpacks="() => {}"
@create="handleServerModpackFlowCreate"
/>
<Teleport to="#sidebar-teleport-target">
<Teleport v-if="browseRouteActive" to="#sidebar-teleport-target">
<BrowseSidebar />
</Teleport>
</div>
@@ -13,7 +13,7 @@ const queryClient = useQueryClient()
if (worldId.value) {
try {
await queryClient.ensureQueryData({
queryKey: ['backups', 'list', serverId],
queryKey: ['backups', 'list', serverId, worldId.value],
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
staleTime: 30_000,
})
@@ -13,7 +13,7 @@ const queryClient = useQueryClient()
if (worldId.value) {
try {
await queryClient.ensureQueryData({
queryKey: ['content', 'list', 'v1', serverId],
queryKey: ['content', 'list', 'v1', serverId, worldId.value],
queryFn: () =>
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
staleTime: 30_000,
@@ -4,7 +4,6 @@
:server-id="serverId"
:reload-page="() => router.go(0)"
:resolve-viewer="resolveViewer"
:show-copy-id-action="themeStore.devMode"
:auth-user="authUser"
:navigate-to-billing="() => openUrl('https://modrinth.com/settings/billing')"
:navigate-to-servers="() => router.push('/hosting/manage')"
@@ -26,7 +25,7 @@
"
>
<template #default="{ onReinstall, onReinstallFailed }">
<RouterView v-slot="{ Component }">
<RouterView v-slot="{ Component }" :route="managedRoute">
<template v-if="Component">
<Suspense>
<component
@@ -47,26 +46,37 @@ import type { Archon, Labrinth } from '@modrinth/api-client'
import { injectAuth, injectModrinthClient, ServersManageRootLayout } from '@modrinth/ui'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, watch } from 'vue'
import { computed, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { get_user } from '@/helpers/cache'
import { get as getCreds } from '@/helpers/mr_auth'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { useTheming } from '@/store/theme'
const route = useRoute()
const router = useRouter()
const auth = injectAuth()
const client = injectModrinthClient()
const queryClient = useQueryClient()
const themeStore = useTheming()
const breadcrumbs = useBreadcrumbs()
const serverId = computed(() => {
const rawId = route.params.id
return Array.isArray(rawId) ? rawId[0] : (rawId ?? '')
})
const managedRoute = shallowRef(router.currentRoute.value)
const serverId = ref(getRouteParam(managedRoute.value.params.id) ?? '')
watch(
router.currentRoute,
(nextRoute) => {
if (!nextRoute.path.startsWith('/hosting/manage/')) return
managedRoute.value = nextRoute
serverId.value = getRouteParam(nextRoute.params.id) ?? ''
},
{ immediate: true },
)
function getRouteParam(param: string | string[] | undefined): string | null {
if (Array.isArray(param)) return param[0] ?? null
return param ?? null
}
if (serverId.value) {
try {
@@ -93,7 +103,7 @@ watch(
breadcrumbs.setName('Server', server.name)
breadcrumbs.setContext({
name: server.name,
link: `/hosting/manage/${serverId.value}/content`,
link: `/hosting/manage/${serverId.value}/instances`,
})
}
},
@@ -0,0 +1,16 @@
<template>
<ServersManageInstanceRootLayout>
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Suspense>
<component :is="Component" />
</Suspense>
</template>
</RouterView>
</ServersManageInstanceRootLayout>
</template>
<script setup lang="ts">
import { ServersManageInstanceRootLayout } from '@modrinth/ui'
import { RouterView } from 'vue-router'
</script>
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { ServersManageInstancesPage } from '@modrinth/ui'
</script>
<template>
<ServersManageInstancesPage />
</template>
@@ -2,6 +2,8 @@ import Backups from './Backups.vue'
import Content from './Content.vue'
import Files from './Files.vue'
import Index from './Index.vue'
import Instance from './Instance.vue'
import Instances from './Instances.vue'
import Overview from './Overview.vue'
export { Backups, Content, Files, Index, Overview }
export { Backups, Content, Files, Index, Instance, Instances, Overview }
+408 -240
View File
@@ -13,200 +13,57 @@
@unlinked="fetchInstance"
/>
<UpdateToPlayModal ref="updateToPlayModal" :instance="instance" />
<ContentPageHeader>
<template #icon>
<Avatar
:src="icon ? icon : undefined"
:alt="instance.name"
size="64px"
:tint-by="instance.path"
/>
</template>
<template #title>
{{ instance.name }}
</template>
<template #stats>
<PageHeader
:header="instance.name"
:leading="instanceHeaderLeading"
:metadata="instanceHeaderMetadata"
:actions="instanceHeaderActions"
>
<template #metadata-server-details>
<div class="flex items-center flex-wrap gap-2">
<template v-if="!isServerInstance">
<div class="flex items-center gap-2 capitalize font-medium">
{{ instance.loader }} {{ instance.game_version }}
</div>
<div class="w-1.5 h-1.5 rounded-full bg-surface-5"></div>
<div class="flex items-center gap-2 font-medium">
<template v-if="timePlayed > 0">
{{ timePlayedHumanized }}
</template>
<template v-else> Never played </template>
</div>
</template>
<template v-else>
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<ServerPing v-if="ping" :ping="ping" />
</template>
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<template v-if="loadingServerPing">
<ServerOnlinePlayers
v-if="playersOnline !== undefined"
:online="playersOnline"
:status-online="statusOnline"
hide-label
/>
<ServerRecentPlays :recent-plays="recentPlays ?? 0" hide-label />
<div
v-if="minecraftServer?.region || ping"
v-if="
(playersOnline !== undefined || recentPlays !== undefined) &&
(minecraftServer?.region || ping)
"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<div
v-if="linkedProjectV3"
class="flex gap-1.5 items-center font-medium text-primary"
>
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instance.path"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
{{ linkedProjectV3.name }}
</router-link>
</div>
<ServerPing v-if="ping" :ping="ping" />
</template>
</div>
</template>
<template #actions>
<div class="flex gap-2">
<ButtonStyled
v-if="
[
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
].includes(instance.install_stage)
"
color="brand"
size="large"
>
<button disabled>Installing...</button>
</ButtonStyled>
<ButtonStyled
v-else-if="instance.install_stage !== 'installed'"
color="brand"
size="large"
>
<button @click="repairInstance()">
<DownloadIcon />
Repair
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing === true" color="red" size="large">
<button :disabled="stopping" @click="stopInstance('InstancePage')">
<StopCircleIcon />
{{ stopping ? 'Stopping...' : 'Stop' }}
</button>
</ButtonStyled>
<ButtonStyled
v-else-if="playing === false && loading === false && !isServerInstance"
color="brand"
size="large"
>
<button @click="startInstance('InstancePage')">
<PlayIcon />
Play
</button>
</ButtonStyled>
<div
v-else-if="playing === false && loading === false && isServerInstance"
class="joined-buttons"
>
<ButtonStyled color="brand" size="large">
<button @click="handlePlayServer()">
<PlayIcon />
Play
</button>
</ButtonStyled>
<ButtonStyled color="brand" size="large">
<OverflowMenu
:options="[
{
id: 'join_server',
action: () => handlePlayServer(),
},
{
id: 'launch_instance',
action: () => startInstance('InstancePage'),
},
]"
>
<div class="w-0 text-xl relative top-0.5 right-2.5">
<DropdownIcon />
</div>
<template #join_server>
<PlayIcon />
Join server
</template>
<template #launch_instance>
<PlayIcon />
Launch instance
</template>
</OverflowMenu>
</ButtonStyled>
</div>
<ButtonStyled
v-else-if="loading === true && playing === false"
color="brand"
size="large"
>
<button disabled>Starting...</button>
</ButtonStyled>
<ButtonStyled circular size="large">
<button v-tooltip="'Instance settings'" @click="settingsModal?.show()">
<SettingsIcon />
</button>
</ButtonStyled>
<ButtonStyled type="transparent" circular size="large">
<OverflowMenu
:options="[
{
id: 'open-folder',
action: () => {
if (instance) showProfileInFolder(instance.path)
},
},
{
id: 'export-mrpack',
action: () => exportModal?.show(),
},
]"
<ServerRegion v-if="minecraftServer?.region" :region="minecraftServer?.region" />
<div
v-if="minecraftServer?.region || ping"
class="w-1.5 h-1.5 rounded-full bg-surface-5"
></div>
<div v-if="linkedProjectV3" class="flex gap-1.5 items-center font-medium text-primary">
Linked to
<Avatar
:src="linkedProjectV3.icon_url"
:alt="linkedProjectV3.name"
:tint-by="instance.path"
size="24px"
/>
<router-link
:to="`/project/${linkedProjectV3.slug ?? linkedProjectV3.id}`"
class="hover:underline text-primary truncate"
>
<MoreVerticalIcon />
<template #share-instance> <UserPlusIcon /> Share instance </template>
<template #host-a-server> <ServerIcon /> Create a server </template>
<template #open-folder> <FolderOpenIcon /> Open folder </template>
<template #export-mrpack> <PackageIcon /> Export modpack </template>
</OverflowMenu>
</ButtonStyled>
{{ linkedProjectV3.name }}
</router-link>
</div>
</div>
</template>
</ContentPageHeader>
</PageHeader>
</div>
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
<NavTabs :links="tabs" />
@@ -218,7 +75,11 @@
:key="instance.path"
>
<template v-if="Component">
<Suspense :key="instance.path">
<Suspense
:key="instance.path"
@pending="subpagePending = true"
@resolve="subpagePending = false"
>
<component
:is="Component"
:instance="instance"
@@ -228,6 +89,7 @@
:installed="instance.install_stage !== 'installed'"
:is-server-instance="isServerInstance"
:open-settings="() => settingsModal?.show(1)"
v-bind="contentSubpageProps"
@play="updatePlayState"
@stop="() => stopInstance('InstanceSubpage')"
></component>
@@ -265,58 +127,64 @@ import {
CheckCircleIcon,
ClipboardCopyIcon,
DownloadIcon,
DropdownIcon,
EditIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
TagCategoryGamepad2Icon as Gamepad2Icon,
GlobeIcon,
HashIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
PlusIcon,
ServerIcon,
SettingsIcon,
StopCircleIcon,
TerminalSquareIcon,
TimerIcon,
UpdatedIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets'
import {
Avatar,
ButtonStyled,
ContentPageHeader,
formatLoaderLabel,
injectNotificationManager,
NavTabs,
OverflowMenu,
PageHeader,
LoaderIcon as ServerLoaderIcon,
ServerOnlinePlayers,
ServerPing,
ServerRecentPlays,
ServerRegion,
useLoadingBarToken,
} from '@modrinth/ui'
import type { Loaders } from '@modrinth/utils'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { computed, onUnmounted, ref } from 'vue'
import { onBeforeRouteUpdate, useRoute, useRouter, type LocationQuery } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import ExportModal from '@/components/ui/ExportModal.vue'
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { trackEvent } from '@/helpers/analytics'
import { get_project_v3 } from '@/helpers/cache.js'
import { process_listener, profile_listener } from '@/helpers/events'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import { get_by_profile_path } from '@/helpers/process'
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import type { GameInstance } from '@/helpers/types'
import { showProfileInFolder } from '@/helpers/utils.js'
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
import { refreshWorlds, type ServerStatus } from '@/helpers/worlds'
import { injectServerInstall } from '@/providers/server-install'
import { handleSevereError } from '@/store/error.js'
import { useBreadcrumbs } from '@/store/state'
@@ -331,6 +199,7 @@ const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
const contentSubpageRouteNames = new Set(['Mods', 'ModsFilter'])
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -341,98 +210,221 @@ window.addEventListener('online', () => {
})
const instance = ref<GameInstance>()
const preloadedContent = ref<InstanceContentData | null>(null)
const playing = ref(false)
const loading = ref(false)
const subpagePending = ref(false)
const stopping = ref(false)
const exportModal = ref<InstanceType<typeof ExportModal>>()
const updateToPlayModal = ref<InstanceType<typeof UpdateToPlayModal>>()
useLoadingBarToken(subpagePending)
const isServerInstance = ref(false)
const linkedProjectV3 = ref<Labrinth.Projects.v3.Project>()
const selected = ref<unknown[]>([])
const minecraftServer = computed(() => linkedProjectV3.value?.minecraft_server)
const javaServerPingData = computed(() => linkedProjectV3.value?.minecraft_java_server?.ping?.data)
const statusOnline = computed(() => !!javaServerPingData.value)
const liveServerStatusOnline = ref(false)
const statusOnline = computed(() => liveServerStatusOnline.value || !!javaServerPingData.value)
const recentPlays = computed(
() => linkedProjectV3.value?.minecraft_java_server?.verified_plays_2w ?? undefined,
)
const playersOnline = ref<number | undefined>(undefined)
const ping = ref<number | undefined>(undefined)
const loadingServerPing = ref(false)
const activeProfilePath = ref<string>()
let fetchInstanceRequestId = 0
async function fetchInstance() {
isServerInstance.value = false
linkedProjectV3.value = undefined
type InstanceRouteContext = {
name: unknown
path: string
query: LocationQuery
}
type InstancePageData = {
profilePath: string
instance?: GameInstance
linkedProjectV3?: Labrinth.Projects.v3.Project
isServerInstance: boolean
preloadedContent: InstanceContentData | null
}
function applyServerStatus(status: ServerStatus) {
playersOnline.value = status.players?.online
ping.value = status.ping
liveServerStatusOnline.value = true
loadingServerPing.value = true
}
function isContentSubpageRoute(routeName: unknown = route.name) {
return typeof routeName === 'string' && contentSubpageRouteNames.has(routeName)
}
function resetServerStatus() {
ping.value = undefined
playersOnline.value = undefined
liveServerStatusOnline.value = false
loadingServerPing.value = false
}
instance.value = await get(route.params.id as string).catch(handleError)
function isCurrentInstanceRequest(requestId: number, profilePath: string) {
return (
requestId === fetchInstanceRequestId &&
route.path.startsWith('/instance') &&
route.params.id === profilePath
)
}
if (!offline.value && instance.value?.linked_data && instance.value.linked_data.project_id) {
function setInstanceBreadcrumbs(nextInstance: GameInstance, routeContext: InstanceRouteContext) {
breadcrumbs.setName(
'Instance',
nextInstance.name.length > 40 ? nextInstance.name.substring(0, 40) + '...' : nextInstance.name,
)
breadcrumbs.setContext({
name: nextInstance.name,
link: routeContext.path,
query: routeContext.query,
})
}
async function loadInstancePageData(
profilePath: string,
routeName: unknown = route.name,
): Promise<InstancePageData> {
const nextInstance = await get(profilePath).catch(handleError)
let nextLinkedProjectV3: Labrinth.Projects.v3.Project | undefined
let nextIsServerInstance = false
const contentPreloadPromise =
nextInstance && isContentSubpageRoute(routeName)
? loadInstanceContentData(nextInstance.path, undefined, handleError)
: Promise.resolve(null)
if (!offline.value && nextInstance?.linked_data && nextInstance.linked_data.project_id) {
try {
linkedProjectV3.value = await get_project_v3(
instance.value.linked_data.project_id,
nextLinkedProjectV3 = await get_project_v3(
nextInstance.linked_data.project_id,
'must_revalidate',
)
if (linkedProjectV3.value?.minecraft_server != null) {
isServerInstance.value = true
if (nextLinkedProjectV3?.minecraft_server != null) {
nextIsServerInstance = true
}
} catch (error) {
handleError(error as Error)
}
}
fetchDeferredData()
const nextPreloadedContent = await contentPreloadPromise
if (instance.value) {
return {
profilePath,
instance: nextInstance ?? undefined,
linkedProjectV3: nextLinkedProjectV3,
isServerInstance: nextIsServerInstance,
preloadedContent: nextPreloadedContent,
}
}
function applyInstancePageData(data: InstancePageData, routeContext: InstanceRouteContext) {
activeProfilePath.value = data.profilePath
resetServerStatus()
playing.value = false
instance.value = data.instance
linkedProjectV3.value = data.linkedProjectV3
isServerInstance.value = data.isServerInstance
preloadedContent.value = data.preloadedContent
if (data.instance) {
setInstanceBreadcrumbs(data.instance, routeContext)
}
fetchDeferredData(data.profilePath)
if (data.instance) {
queryClient.prefetchQuery({
queryKey: ['worlds', instance.value.path],
queryFn: () => refreshWorlds(instance.value!.path),
queryKey: ['worlds', data.instance.path],
queryFn: () => refreshWorlds(data.instance!.path),
staleTime: 30_000,
})
}
}
function fetchDeferredData() {
async function fetchInstance(profilePath = route.params.id as string) {
const requestId = ++fetchInstanceRequestId
const data = await loadInstancePageData(profilePath)
if (!isCurrentInstanceRequest(requestId, profilePath)) return
applyInstancePageData(data, {
name: route.name,
path: route.path,
query: route.query,
})
}
function fetchDeferredData(profilePath: string) {
const serverAddress = linkedProjectV3.value?.minecraft_java_server?.address
if (isServerInstance.value && serverAddress) {
get_server_status(serverAddress)
const cachedStatus = getFreshCachedServerStatus(queryClient, serverAddress)
if (cachedStatus) {
applyServerStatus(cachedStatus)
} else {
playersOnline.value = undefined
ping.value = undefined
loadingServerPing.value = false
}
fetchCachedServerStatus(queryClient, serverAddress)
.then((status) => {
playersOnline.value = status.players?.online
ping.value = status.ping
if (
activeProfilePath.value !== profilePath ||
linkedProjectV3.value?.minecraft_java_server?.address !== serverAddress
)
return
applyServerStatus(status)
})
.catch((error) => {
console.error(`Failed to fetch server status for ${serverAddress}:`, error)
})
.finally(() => {
if (activeProfilePath.value !== profilePath) return
loadingServerPing.value = true
})
} else {
loadingServerPing.value = true
}
updatePlayState()
updatePlayState(profilePath)
}
async function updatePlayState() {
if (!route.params.id) return
const runningProcesses = await get_by_profile_path(route.params.id as string).catch(handleError)
async function updatePlayState(profilePath = route.params.id as string) {
if (!profilePath) return
const runningProcesses = await get_by_profile_path(profilePath).catch(handleError)
if (activeProfilePath.value !== profilePath) return
playing.value = Array.isArray(runningProcesses) && runningProcesses.length > 0
}
await fetchInstance()
watch(
() => route.params.id,
async () => {
if (route.params.id && route.path.startsWith('/instance')) {
await fetchInstance()
}
},
)
await fetchInstance(route.params.id as string)
onBeforeRouteUpdate(async (to) => {
if (!to.path.startsWith('/instance')) return
const profilePath = Array.isArray(to.params.id) ? to.params.id[0] : to.params.id
if (typeof profilePath !== 'string') return false
const requestId = ++fetchInstanceRequestId
const data = await loadInstancePageData(profilePath, to.name)
if (requestId !== fetchInstanceRequestId) return false
applyInstancePageData(data, {
name: to.name,
path: to.path,
query: to.query,
})
})
const basePath = computed(() => `/instance/${encodeURIComponent(route.params.id as string)}`)
@@ -448,6 +440,9 @@ const renderMode = computed<'scroll' | 'fixed'>(() =>
route.meta.renderMode === 'fixed' ? 'fixed' : 'scroll',
)
const isFixedRender = computed(() => renderMode.value === 'fixed')
const contentSubpageProps = computed(() =>
isContentSubpageRoute() ? { preloadedContent: preloadedContent.value } : {},
)
const tabs = computed(() => [
{
@@ -472,20 +467,6 @@ const tabs = computed(() => [
},
])
if (instance.value) {
breadcrumbs.setName(
'Instance',
instance.value.name.length > 40
? instance.value.name.substring(0, 40) + '...'
: instance.value.name,
)
breadcrumbs.setContext({
name: instance.value.name,
link: route.path,
query: route.query,
})
}
const options = ref<InstanceType<typeof ContextMenu> | null>(null)
const startInstance = async (context: string) => {
@@ -645,6 +626,16 @@ const timePlayed = computed(() => {
: 0
})
const loaderDisplayName = computed(() =>
instance.value ? (formatLoaderLabel(instance.value.loader) as Loaders) : null,
)
const loaderLabel = computed(() =>
instance.value && loaderDisplayName.value
? [loaderDisplayName.value, instance.value.loader_version].filter(Boolean).join(' ')
: '',
)
const timePlayedHumanized = computed(() => {
const duration = dayjs.duration(timePlayed.value, 'seconds')
const hours = Math.floor(duration.asHours())
@@ -661,6 +652,183 @@ const timePlayedHumanized = computed(() => {
return seconds + ' second' + (seconds > 1 ? 's' : '')
})
const playtimeLabel = computed(() =>
timePlayed.value > 0 ? timePlayedHumanized.value : 'Never played',
)
const instanceHeaderLeading = computed(() => ({
type: 'avatar' as const,
src: icon.value ? icon.value : undefined,
alt: instance.value?.name,
avatarSize: '64px',
tintBy: instance.value?.path,
}))
const instanceHeaderMetadata = computed(() => {
if (!instance.value) return []
if (isServerInstance.value) {
return [
{
id: 'server-details',
type: 'custom' as const,
class: 'contents',
},
]
}
return [
{
id: 'game-version',
label: instance.value.game_version,
icon: Gamepad2Icon,
},
{
id: 'loader',
label: loaderLabel.value,
icon: ServerLoaderIcon,
iconProps: {
loader: loaderDisplayName.value,
},
},
{
id: 'playtime',
label: playtimeLabel.value,
icon: TimerIcon,
},
]
})
const installingStages = [
'installing',
'pack_installing',
'pack_installed',
'not_installed',
'minecraft_installing',
]
const primaryInstanceAction = computed(() => {
if (!instance.value) return null
if (installingStages.includes(instance.value.install_stage)) {
return {
id: 'installing',
label: 'Installing...',
color: 'brand' as const,
disabled: true,
}
}
if (instance.value.install_stage !== 'installed') {
return {
id: 'repair',
label: 'Repair',
icon: DownloadIcon,
color: 'brand' as const,
onClick: () => {
void repairInstance()
},
}
}
if (playing.value === true) {
return {
id: 'stop',
label: stopping.value ? 'Stopping...' : 'Stop',
icon: StopCircleIcon,
color: 'red' as const,
disabled: stopping.value,
onClick: () => {
void stopInstance('InstancePage')
},
}
}
if (playing.value === false && loading.value === false && !isServerInstance.value) {
return {
id: 'play',
label: 'Play',
icon: PlayIcon,
color: 'brand' as const,
onClick: () => {
void startInstance('InstancePage')
},
}
}
if (playing.value === false && loading.value === false && isServerInstance.value) {
return {
id: 'play',
label: 'Play',
color: 'brand' as const,
joinedActions: [
{
id: 'join_server',
label: 'Play',
icon: PlayIcon,
action: () => {
void handlePlayServer()
},
},
{
id: 'launch_instance',
label: 'Launch instance',
icon: PlayIcon,
action: () => {
void startInstance('InstancePage')
},
},
],
}
}
if (loading.value === true && playing.value === false) {
return {
id: 'starting',
label: 'Starting...',
color: 'brand' as const,
disabled: true,
}
}
return null
})
const instanceHeaderActions = computed(() => [
...(primaryInstanceAction.value ? [primaryInstanceAction.value] : []),
{
id: 'settings',
label: 'Instance settings',
icon: SettingsIcon,
labelHidden: true,
tooltip: 'Instance settings',
onClick: () => settingsModal.value?.show(),
},
{
id: 'more',
label: 'More actions',
icon: MoreVerticalIcon,
labelHidden: true,
type: 'transparent' as const,
tooltip: 'More actions',
menuActions: [
{
id: 'open-folder',
label: 'Open folder',
icon: FolderOpenIcon,
action: () => {
if (instance.value) void showProfileInFolder(instance.value.path)
},
},
{
id: 'export-mrpack',
label: 'Export modpack',
icon: PackageIcon,
action: () => exportModal.value?.show(),
},
],
},
])
onUnmounted(() => {
unlistenProcesses()
unlistenProfiles()
+400 -109
View File
@@ -13,6 +13,7 @@
:modpack-name="linkedModpackProject?.title"
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
:enable-toggle="!props.isServerInstance"
:busy="isBulkOperating"
:get-overflow-options="getOverflowOptions"
:switch-version="handleSwitchVersion"
@update:enabled="handleModpackContentToggle"
@@ -87,12 +88,12 @@ import {
ReadyTransition,
useDebugLogger,
useVIntl,
versionChangesGameVersion,
} from '@modrinth/ui'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { open } from '@tauri-apps/plugin-dialog'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useDebounceFn } from '@vueuse/core'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
@@ -100,22 +101,20 @@ import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache.js'
import { profile_listener } from '@/helpers/events.js'
import { type InstanceContentData, loadInstanceContentData } from '@/helpers/instance-content'
import {
add_project_from_path,
add_project_from_version,
duplicate,
edit,
get,
get_content_items,
get_linked_modpack_content,
get_linked_modpack_info,
list,
remove_project,
toggle_disable_project,
update_managed_modrinth_version,
update_project,
} from '@/helpers/profile'
import { get_categories } from '@/helpers/tags.js'
import type { CacheBehaviour, GameInstance } from '@/helpers/types'
import { highlightModInProfile } from '@/helpers/utils.js'
import { injectContentInstall } from '@/providers/content-install'
@@ -160,6 +159,7 @@ const props = defineProps<{
instance: GameInstance
isServerInstance?: boolean
openSettings?: () => void
preloadedContent?: InstanceContentData | null
}>()
const loading = ref(true)
@@ -212,6 +212,7 @@ const contentUpdaterModal = ref<InstanceType<typeof ContentUpdaterModal> | null>
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal> | null>()
const modpackUpdateConfirmModal = ref<InstanceType<typeof ConfirmModpackUpdateModal> | null>()
// TODO: Extract content operation and updater modal state into composables; this page currently owns file mutations, dependency installs, busy flags, and version selection flow.
const updatingProject = ref<ContentItem | null>(null)
const updatingProjectVersions = ref<Labrinth.Versions.v2.Version[]>([])
const loadingVersions = ref(false)
@@ -219,6 +220,82 @@ const loadingChangelog = ref(false)
const updatingModpack = ref(false)
const pendingModpackUpdateVersion = ref<Labrinth.Versions.v2.Version | null>(null)
const isModpackUpdateDowngrade = ref(false)
const activeContentOperationKeys = ref(new Set<string>())
let activeContentOperationCount = 0
let updateRequestId = 0
const activeUpdateRequestId = ref(0)
function fileNameFromPath(path: string) {
return path.split('/').pop() ?? path
}
function getContentItemId(item: ContentItem | null | undefined) {
return item?.file_path ?? item?.file_name ?? item?.id ?? ''
}
function getContentOperationKeys(item: ContentItem) {
return [getContentItemId(item), item.file_path, item.file_name].filter(
(key): key is string => !!key,
)
}
function hasContentOperation(item: ContentItem) {
const keys = getContentOperationKeys(item)
return keys.some((key) => activeContentOperationKeys.value.has(key))
}
function setContentItemBusy(item: ContentItem, busy: boolean, originalFileName = item.file_name) {
item.installing = busy
modpackContentModal.value?.updateItem(originalFileName, {
installing: busy,
disabled: busy,
})
if (item.file_name !== originalFileName) {
modpackContentModal.value?.updateItem(item.file_name, {
installing: busy,
disabled: busy,
})
}
}
function beginContentOperation(item: ContentItem) {
if (hasContentOperation(item)) return null
const keys = getContentOperationKeys(item)
activeContentOperationKeys.value = new Set([...activeContentOperationKeys.value, ...keys])
activeContentOperationCount++
isBulkOperating.value = true
setContentItemBusy(item, true)
return { keys, originalFileName: item.file_name }
}
function finishContentOperation(
item: ContentItem,
operation: { keys: string[]; originalFileName: string },
) {
const nextKeys = new Set(activeContentOperationKeys.value)
for (const key of operation.keys) {
nextKeys.delete(key)
}
activeContentOperationKeys.value = nextKeys
activeContentOperationCount = Math.max(0, activeContentOperationCount - 1)
setContentItemBusy(item, false, operation.originalFileName)
if (activeContentOperationCount === 0) {
isBulkOperating.value = false
}
}
function beginUpdateRequest() {
updateRequestId++
activeUpdateRequestId.value = updateRequestId
return updateRequestId
}
function isActiveUpdateRequest(requestId: number) {
return activeUpdateRequestId.value === requestId
}
async function handleBrowseContent() {
if (!props.instance) return
@@ -265,9 +342,21 @@ async function handleUploadFiles() {
}
async function toggleDisableMod(mod: ContentItem) {
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
try {
mod.file_path = await toggle_disable_project(props.instance.path, mod.file_path!)
const newPath = await toggle_disable_project(props.instance.path, mod.file_path)
const newFileName = fileNameFromPath(newPath)
mod.file_path = newPath
mod.file_name = newFileName
mod.enabled = !mod.enabled
modpackContentModal.value?.updateItem(operation.originalFileName, {
file_path: newPath,
file_name: newFileName,
enabled: mod.enabled,
})
trackEvent('InstanceProjectDisable', {
loader: props.instance.loader,
@@ -279,33 +368,48 @@ async function toggleDisableMod(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(mod, operation)
}
}
const toggleDisableDebounced = useDebounceFn(toggleDisableMod, 20)
const toggleDisableDebounced = toggleDisableMod
async function removeMod(mod: ContentItem) {
await remove_project(props.instance.path, mod.file_path!).catch(handleError)
projects.value = projects.value.filter((x) => mod.file_path !== x.file_path)
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
try {
const removedPath = mod.file_path
await remove_project(props.instance.path, removedPath)
projects.value = projects.value.filter((x) => removedPath !== x.file_path)
trackEvent('InstanceProjectRemove', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
} catch (err) {
handleError(err as Error)
} finally {
finishContentOperation(mod, operation)
}
}
async function updateProject(mod: ContentItem) {
try {
const newPath = await update_project(props.instance.path, mod.file_path!)
mod.file_path = newPath
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
if (mod.update_version_id) {
const versionData = await get_version(mod.update_version_id, 'must_revalidate').catch(
handleError,
)
try {
const updateVersionId = mod.update_version_id
await update_project(props.instance.path, mod.file_path)
if (updateVersionId) {
const versionData = await get_version(updateVersionId, 'must_revalidate').catch(handleError)
if (versionData) {
const profile = await get(props.instance.path).catch(handleError)
@@ -316,12 +420,6 @@ async function updateProject(mod: ContentItem) {
}
}
mod.has_update = false
if (mod.version && mod.update_version_id) {
mod.version.id = mod.update_version_id
}
mod.update_version_id = null
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
@@ -331,39 +429,65 @@ async function updateProject(mod: ContentItem) {
})
} catch (err) {
handleError(err as Error)
} finally {
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
}
}
async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions.v2.Version) {
isBulkOperating.value = true
mod.installing = true
if (mod.version) {
mod.version.id = version.id
mod.version.version_number = version.version_number
}
if (!mod.file_path) return
const operation = beginContentOperation(mod)
if (!operation) return
const oldPath = mod.file_path
const wasDisabled = mod.enabled === false || oldPath.endsWith('.disabled')
let newPath: string | null = null
let shouldRemoveNewOnError = false
try {
await remove_project(props.instance.path, mod.file_path!)
const newPath = await add_project_from_version(props.instance.path, version.id, 'standalone')
newPath = await add_project_from_version(props.instance.path, version.id, 'update')
shouldRemoveNewOnError = newPath !== oldPath
if (wasDisabled) {
newPath = await toggle_disable_project(props.instance.path, newPath)
}
const profile = await get(props.instance.path).catch(handleError)
if (profile) {
await installVersionDependencies(profile, version, 'update').catch(handleError)
}
mod.file_path = newPath
shouldRemoveNewOnError = false
if (newPath !== oldPath) {
await remove_project(props.instance.path, oldPath)
}
trackEvent('InstanceProjectUpdate', {
loader: props.instance.loader,
game_version: props.instance.game_version,
id: mod.project?.id,
name: mod.project?.title ?? mod.file_name,
project_type: mod.project_type,
})
} catch (err) {
if (shouldRemoveNewOnError && newPath && newPath !== oldPath) {
await remove_project(props.instance.path, newPath).catch(() => {})
}
handleError(err as Error)
} finally {
mod.installing = false
isBulkOperating.value = false
await initProjects()
await refreshContentState('must_revalidate')
finishContentOperation(mod, operation)
}
}
async function handleUpdate(id: string) {
const item = projects.value.find((p) => p.id === id)
const item = projects.value.find((p) => getContentItemId(p) === id)
if (!item?.has_update || !item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
debug('handleUpdate triggered', {
fileName: item.file_name,
projectType: item.project_type,
@@ -384,12 +508,48 @@ async function handleUpdate(id: string) {
await nextTick()
contentUpdaterModal.value?.show(item.update_version_id ?? undefined)
const initialVersionId = item.update_version_id ?? undefined
debug('handleUpdate: opening content updater modal', {
type: 'content',
initialVersionId,
item: {
id: item.id,
fileName: item.file_name,
projectType: item.project_type,
projectId: item.project.id,
projectTitle: item.project.title,
currentVersionId: item.version.id,
currentVersionNumber: item.version.version_number,
updateVersionId: item.update_version_id,
},
instance: {
path: props.instance.path,
name: props.instance.name,
gameVersion: props.instance.game_version,
loader: props.instance.loader,
linkedData: props.instance.linked_data,
},
modalStateBeforeFetch: {
updatingModpack: updatingModpack.value,
updatingProjectId: updatingProject.value?.id,
updatingProjectVersions: updatingProjectVersions.value.map((version) => ({
id: version.id,
versionNumber: version.version_number,
gameVersions: version.game_versions,
loaders: version.loaders,
datePublished: version.date_published,
})),
},
})
contentUpdaterModal.value?.show(initialVersionId)
const versions = (await get_project_versions(item.project.id).catch((e) => {
return handleError(e)
})) as Labrinth.Versions.v2.Version[] | null
if (!isActiveUpdateRequest(requestId) || getContentItemId(updatingProject.value) !== itemId)
return
loadingVersions.value = false
if (!versions) {
@@ -414,6 +574,25 @@ async function handleUpdate(id: string) {
versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
const preselectedVersion =
versions.find((version) => version.id === initialVersionId) ?? versions[0] ?? null
debug('handleUpdate: resolved content updater preselection', {
type: 'content',
initialVersionId,
foundInitialVersion: versions.some((version) => version.id === initialVersionId),
preselectedVersion: preselectedVersion
? {
id: preselectedVersion.id,
versionNumber: preselectedVersion.version_number,
gameVersions: preselectedVersion.game_versions,
loaders: preselectedVersion.loaders,
datePublished: preselectedVersion.date_published,
}
: null,
versionCount: versions.length,
currentVersionId: item.version.id,
updateVersionId: item.update_version_id,
})
updatingProjectVersions.value = versions
}
@@ -421,6 +600,9 @@ async function handleUpdate(id: string) {
async function handleSwitchVersion(item: ContentItem) {
if (!item.project?.id || !item.version?.id) return
const requestId = beginUpdateRequest()
const itemId = getContentItemId(item)
updatingModpack.value = false
updatingProject.value = item
updatingProjectVersions.value = []
@@ -435,6 +617,9 @@ async function handleSwitchVersion(item: ContentItem) {
return handleError(e)
})) as Labrinth.Versions.v2.Version[] | null
if (!isActiveUpdateRequest(requestId) || getContentItemId(updatingProject.value) !== itemId)
return
loadingVersions.value = false
if (!versions) return
@@ -468,9 +653,28 @@ async function handleModpackContent() {
}
}
async function refreshModpackContentItems(cacheBehaviour?: CacheBehaviour) {
if (!props.instance?.path) return
const contentItems = await get_linked_modpack_content(props.instance.path, cacheBehaviour).catch(
handleError,
)
if (contentItems) {
modpackContentModal.value?.setItems(contentItems)
}
}
async function refreshContentState(cacheBehaviour?: CacheBehaviour) {
await initProjects(cacheBehaviour)
await refreshModpackContentItems(cacheBehaviour)
}
async function handleModpackUpdate() {
if (!props.instance?.linked_data?.project_id) return
const requestId = beginUpdateRequest()
updatingModpack.value = true
updatingProject.value = null
updatingProjectVersions.value = []
@@ -479,14 +683,42 @@ async function handleModpackUpdate() {
await nextTick()
contentUpdaterModal.value?.show(
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined,
)
const initialVersionId =
linkedModpackUpdateVersionId.value ?? props.instance?.linked_data?.version_id ?? undefined
debug('handleModpackUpdate: opening modpack updater modal', {
type: 'modpack',
initialVersionId,
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
linkedModpackProject: linkedModpackProject.value,
linkedModpackVersion: linkedModpackVersion.value,
linkedModpackHasUpdate: linkedModpackHasUpdate.value,
instance: {
path: props.instance.path,
name: props.instance.name,
gameVersion: props.instance.game_version,
loader: props.instance.loader,
linkedData: props.instance.linked_data,
},
modalStateBeforeFetch: {
updatingModpack: updatingModpack.value,
updatingProjectId: updatingProject.value?.id,
updatingProjectVersions: updatingProjectVersions.value.map((version) => ({
id: version.id,
versionNumber: version.version_number,
gameVersions: version.game_versions,
loaders: version.loaders,
datePublished: version.date_published,
})),
},
})
contentUpdaterModal.value?.show(initialVersionId)
const versions = (await get_project_versions(props.instance.linked_data.project_id).catch(
handleError,
)) as Labrinth.Versions.v2.Version[] | null
if (!isActiveUpdateRequest(requestId) || !updatingModpack.value) return
loadingVersions.value = false
if (!versions) return
@@ -494,6 +726,25 @@ async function handleModpackUpdate() {
versions.sort(
(a, b) => new Date(b.date_published).getTime() - new Date(a.date_published).getTime(),
)
const preselectedVersion =
versions.find((version) => version.id === initialVersionId) ?? versions[0] ?? null
debug('handleModpackUpdate: resolved modpack updater preselection', {
type: 'modpack',
initialVersionId,
foundInitialVersion: versions.some((version) => version.id === initialVersionId),
preselectedVersion: preselectedVersion
? {
id: preselectedVersion.id,
versionNumber: preselectedVersion.version_number,
gameVersions: preselectedVersion.game_versions,
loaders: preselectedVersion.loaders,
datePublished: preselectedVersion.date_published,
}
: null,
versionCount: versions.length,
linkedModpackUpdateVersionId: linkedModpackUpdateVersionId.value,
currentLinkedVersionId: props.instance.linked_data.version_id,
})
updatingProjectVersions.value = versions
}
@@ -502,10 +753,12 @@ async function fetchAndSpliceVersion(
versionId: string,
cacheBehaviour?: Parameters<typeof get_version>[1],
onError?: (err: unknown) => void,
requestId = activeUpdateRequestId.value,
) {
const fullVersion = (await get_version(versionId, cacheBehaviour).catch(
onError ?? (() => null),
)) as Labrinth.Versions.v2.Version | null
if (!isActiveUpdateRequest(requestId)) return
if (!fullVersion) return
const index = updatingProjectVersions.value.findIndex((v) => v.id === versionId)
if (index !== -1) {
@@ -517,17 +770,26 @@ async function fetchAndSpliceVersion(
async function handleVersionSelect(version: Labrinth.Versions.v2.Version) {
if (version.changelog != null) return
const requestId = activeUpdateRequestId.value
loadingChangelog.value = true
await fetchAndSpliceVersion(version.id, 'must_revalidate', handleError as (err: unknown) => void)
loadingChangelog.value = false
await fetchAndSpliceVersion(
version.id,
'must_revalidate',
handleError as (err: unknown) => void,
requestId,
)
if (isActiveUpdateRequest(requestId)) {
loadingChangelog.value = false
}
}
async function handleVersionHover(version: Labrinth.Versions.v2.Version) {
if (version.changelog != null) return
await fetchAndSpliceVersion(version.id)
await fetchAndSpliceVersion(version.id, undefined, undefined, activeUpdateRequestId.value)
}
function resetUpdateState() {
activeUpdateRequestId.value = 0
updatingModpack.value = false
updatingProject.value = null
updatingProjectVersions.value = []
@@ -535,13 +797,23 @@ function resetUpdateState() {
loadingChangelog.value = false
}
function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
async function handleModpackUpdateRequest(selectedVersion: Labrinth.Versions.v2.Version) {
pendingModpackUpdateVersion.value = selectedVersion
const currentVersionId = props.instance?.linked_data?.version_id
const currentVersion = updatingProjectVersions.value.find((v) => v.id === currentVersionId)
isModpackUpdateDowngrade.value = currentVersion
? new Date(selectedVersion.date_published) < new Date(currentVersion.date_published)
: false
const shouldShowWarning =
isModpackUpdateDowngrade.value ||
versionChangesGameVersion(selectedVersion, props.instance.game_version)
if (!shouldShowWarning) {
await handleModpackUpdateConfirm()
return
}
modpackUpdateConfirmModal.value?.show()
}
@@ -551,6 +823,7 @@ async function handleModpackUpdateConfirm() {
const version = pendingModpackUpdateVersion.value
pendingModpackUpdateVersion.value = null
contentUpdaterModal.value?.hide()
isModpackUpdating.value = true
try {
await update_managed_modrinth_version(props.instance.path, version.id)
@@ -574,7 +847,7 @@ async function handleModalUpdate(
pendingModpackUpdateVersion.value = selectedVersion
await handleModpackUpdateConfirm()
} else {
handleModpackUpdateRequest(selectedVersion)
await handleModpackUpdateRequest(selectedVersion)
}
} else if (updatingProject.value) {
const mod = updatingProject.value
@@ -662,51 +935,31 @@ function getOverflowOptions(item: ContentItem): OverflowMenuOption[] {
async function initProjects(cacheBehaviour?: CacheBehaviour) {
if (!props.instance) return
const [contentItems, modpackInfo, allCategories] = await Promise.all([
get_content_items(props.instance.path, cacheBehaviour).catch(handleError),
get_linked_modpack_info(props.instance.path, cacheBehaviour).catch(handleError),
get_categories().catch(handleError),
])
const contentData = await loadInstanceContentData(
props.instance.path,
cacheBehaviour,
handleError,
)
applyContentData(contentData)
}
if (!contentItems) {
function applyContentData(contentData: InstanceContentData) {
if (contentData.path !== props.instance.path) return false
if (!contentData.contentItems) {
loading.value = false
return
return true
}
projects.value = contentItems
projects.value = contentData.contentItems
if (modpackInfo) {
linkedModpackProject.value = {
...modpackInfo.project,
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
icon_url: modpackInfo.project.icon_url ?? undefined,
}
linkedModpackVersion.value = {
...modpackInfo.version,
date_published: modpackInfo.version.date_published.toString(),
}
linkedModpackOwner.value = modpackInfo.owner
? {
...modpackInfo.owner,
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
}
: null
linkedModpackHasUpdate.value = modpackInfo.has_update
linkedModpackUpdateVersionId.value = modpackInfo.update_version_id
if (allCategories && modpackInfo.project.categories) {
const seen = new Set<string>()
linkedModpackCategories.value = allCategories.filter((cat: { name: string }) => {
if (modpackInfo.project.categories.includes(cat.name) && !seen.has(cat.name)) {
seen.add(cat.name)
return true
}
return false
})
} else {
linkedModpackCategories.value = []
}
if (contentData.modpack) {
linkedModpackProject.value = contentData.modpack.project
linkedModpackVersion.value = contentData.modpack.version
linkedModpackOwner.value = contentData.modpack.owner
linkedModpackCategories.value = contentData.modpack.categories
linkedModpackHasUpdate.value = contentData.modpack.hasUpdate
linkedModpackUpdateVersionId.value = contentData.modpack.updateVersionId
} else {
linkedModpackProject.value = null
linkedModpackVersion.value = null
@@ -717,6 +970,7 @@ async function initProjects(cacheBehaviour?: CacheBehaviour) {
}
loading.value = false
return true
}
provideAppBackup({
@@ -809,8 +1063,9 @@ provideContentManager({
showContentHint,
dismissContentHint,
shareItems: handleShareItems,
getItemId: getContentItemId,
mapToTableItem: (item: ContentItem) => ({
id: item.id,
id: getContentItemId(item),
project: item.project ?? {
id: item.file_name,
slug: null,
@@ -844,10 +1099,22 @@ provideContentManager({
filterPersistKey: props.instance.path,
})
await initProjects()
type UnlistenFn = () => void
const initialContentReady = loadInitialContent()
void initialContentReady.then(restoreModpackContentModalState).catch(handleError)
function loadInitialContent() {
if (props.preloadedContent && applyContentData(props.preloadedContent)) {
return Promise.resolve()
}
return initProjects()
}
async function restoreModpackContentModalState() {
if (!savedModalState) return
// Restore modpack content modal state if returning via back navigation
if (savedModalState) {
const stateToRestore = savedModalState
savedModalState = null
await nextTick()
@@ -860,18 +1127,32 @@ const removeBeforeEach = router.beforeEach(() => {
savedModalState = state ?? null
})
const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
let isUnmounted = false
let unlistenDragDrop: UnlistenFn | null = null
let unlistenProfiles: UnlistenFn | null = null
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError)
}
await initProjects()
})
onMounted(() => {
void getCurrentWebview()
.onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop' || !props.instance) return
const unlistenProfiles = await profile_listener(
async (event: { event: string; profile_path_id: string }) => {
for (const file of event.payload.paths) {
if (file.endsWith('.mrpack')) continue
await add_project_from_path(props.instance.path, file).catch(handleError)
}
await initProjects()
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenDragDrop = unlisten
})
.catch(handleError)
void profile_listener(async (event: { event: string; profile_path_id: string }) => {
if (
props.instance &&
event.profile_path_id === props.instance.path &&
@@ -881,8 +1162,17 @@ const unlistenProfiles = await profile_listener(
) {
await initProjects()
}
},
)
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenProfiles = unlisten
})
.catch(handleError)
})
watch(
() => props.instance?.install_stage,
@@ -905,8 +1195,9 @@ watch(
)
onUnmounted(() => {
isUnmounted = true
removeBeforeEach()
unlisten()
unlistenProfiles()
unlistenDragDrop?.()
unlistenProfiles?.()
})
</script>
+161 -127
View File
@@ -47,9 +47,9 @@
<div class="flex flex-col gap-4 p-6">
<div
v-if="projectInstallContext"
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 p-3 border-surface-5"
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid border-divider bg-surface-1 px-6 pt-6"
>
<BrowseInstallHeader :install-context="projectInstallContext" />
<BrowseInstallHeader :install-context="projectInstallContext" bottom-padding />
</div>
<InstanceIndicator v-if="instance && !projectInstallContext" :instance="instance" />
<template v-if="data">
@@ -64,121 +64,9 @@
:project="data"
:project-v3="projectV3"
:ping="serverPing"
:actions="projectHeaderActions"
@contextmenu.prevent.stop="handleRightClick"
>
<template v-if="isServerProject" #actions>
<ButtonStyled v-if="serverPlaying" size="large" color="red">
<button @click="handleStopServer">
<StopCircleIcon />
{{ formatMessage(commonMessages.stopButton) }}
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand">
<button
:disabled="data && installingServerProjects.includes(data.id)"
@click="handleClickPlay"
>
<PlayIcon />
{{
data && installingServerProjects.includes(data.id)
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton)
}}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular>
<button
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
@click="handleAddServerToInstance"
>
<PlusIcon />
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:tooltip="`More options`"
:options="[
{
id: 'open-in-browser',
link: `https://modrinth.com/project/${data.slug}`,
external: true,
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
<template #report> <ReportIcon /> Report </template>
</OverflowMenu>
</ButtonStyled>
</template>
<template v-else #actions>
<ButtonStyled size="large" color="brand">
<button
v-tooltip="installButtonTooltip"
:disabled="installButtonDisabled"
@click="install(null)"
>
<SpinnerIcon
v-if="installButtonLoading && !installButtonInstalled"
class="animate-spin"
/>
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
<CheckIcon v-else />
{{ installButtonLabel }}
</button>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:tooltip="`More options`"
:options="[
{
id: 'follow',
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'save',
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'open-in-browser',
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
external: true,
},
{
divider: true,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
<template #follow> <HeartIcon /> Follow </template>
<template #save> <BookmarkIcon /> Save </template>
<template #report> <ReportIcon /> Report </template>
</OverflowMenu>
</ButtonStyled>
</template>
</ProjectHeader>
/>
<NavTabs
:links="[
{
@@ -266,14 +154,12 @@ import {
} from '@modrinth/assets'
import {
BrowseInstallHeader,
ButtonStyled,
commonMessages,
CreationFlowModal,
defineMessages,
getTargetInstallPreferences,
injectNotificationManager,
NavTabs,
OverflowMenu,
ProjectBackgroundGradient,
ProjectHeader,
ProjectSidebarCompatibility,
@@ -286,6 +172,7 @@ import {
SelectedProjectsFloatingBar,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import dayjs from 'dayjs'
@@ -295,6 +182,10 @@ import { useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import {
get_organization,
get_project,
@@ -313,7 +204,6 @@ import {
list as listInstances,
} from '@/helpers/profile'
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
import { getServerLatency } from '@/helpers/worlds'
import { injectContentInstall } from '@/providers/content-install'
import { injectServerInstall } from '@/providers/server-install'
import { createServerInstallContent } from '@/providers/setup/server-install-content'
@@ -327,6 +217,7 @@ const { handleError } = injectNotificationManager()
const { install: installVersion } = injectContentInstall()
const route = useRoute()
const router = useRouter()
const queryClient = useQueryClient()
const breadcrumbs = useBreadcrumbs()
const themeStore = useTheming()
const { formatMessage } = useVIntl()
@@ -334,12 +225,16 @@ const { formatMessage } = useVIntl()
const messages = defineMessages({
backToBrowse: {
id: 'app.project.install-context.back-to-browse',
defaultMessage: 'Back to browse',
defaultMessage: 'Back to discover',
},
installContentToInstance: {
id: 'app.project.install-context.install-content-to-instance',
defaultMessage: 'Install content to instance',
},
worldFallbackName: {
id: 'app.project.install-context.world-fallback-name',
defaultMessage: 'Instance',
},
alreadyInstalled: {
id: 'app.project.install-button.already-installed',
defaultMessage: 'This project is already installed',
@@ -370,7 +265,10 @@ const serverStatusOnline = ref(false)
const serverInstancePath = ref(null)
const serverPlaying = ref(false)
const serverSetupModalRef = ref(null)
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
const serverInstallContent = createServerInstallContent({
serverSetupModalRef,
isRouteInContext: (targetRoute) => targetRoute.path.startsWith('/project/'),
})
serverInstallContent.watchServerContextChanges()
await serverInstallContent.initServerContext()
@@ -415,7 +313,7 @@ const projectGalleryHref = computed(() => buildProjectHref(`/project/${route.par
const projectBrowseBackUrl = computed(() => {
const browsePath = route.query.b
if (typeof browsePath === 'string' && browsePath.startsWith('/browse/')) return browsePath
const type = data.value?.project_type ? `${data.value.project_type}s` : 'mods'
const type = data.value?.project_type ? `${data.value.project_type}` : 'mod'
return `/browse/${type}`
})
@@ -423,7 +321,9 @@ const projectInstallContext = computed(() => {
const serverData = serverInstallContent.serverContextServerData.value
if (serverData) {
return {
name: serverData.name,
name:
serverInstallContent.serverContextWorldName.value ??
formatMessage(messages.worldFallbackName),
loader: serverData.loader ?? '',
gameVersion: serverData.mc_version ?? '',
serverId: serverInstallContent.serverIdQuery.value,
@@ -501,6 +401,131 @@ const installButtonTooltip = computed(() => {
return null
})
const projectHeaderActions = computed(() => {
if (!data.value) return []
if (isServerProject.value) {
return [
serverPlaying.value
? {
id: 'stop',
label: formatMessage(commonMessages.stopButton),
icon: StopCircleIcon,
color: 'red',
onClick: handleStopServer,
}
: {
id: 'play',
label:
data.value && installingServerProjects.value.includes(data.value.id)
? formatMessage(commonMessages.installingLabel)
: formatMessage(commonMessages.playButton),
icon: PlayIcon,
color: 'brand',
disabled: data.value && installingServerProjects.value.includes(data.value.id),
onClick: handleClickPlay,
},
{
id: 'add-server-to-instance',
label: formatMessage(commonMessages.addServerToInstanceButton),
icon: PlusIcon,
labelHidden: true,
tooltip: formatMessage(commonMessages.addServerToInstanceButton),
onClick: handleAddServerToInstance,
},
{
id: 'more',
label: 'More options',
icon: MoreVerticalIcon,
labelHidden: true,
type: 'transparent',
tooltip: 'More options',
menuActions: [
{
id: 'open-in-browser',
label: 'Open in browser',
icon: ExternalIcon,
action: () => openUrl(`https://modrinth.com/project/${data.value.slug}`),
},
{
divider: true,
},
{
id: 'report',
label: 'Report',
icon: ReportIcon,
color: 'red',
action: () =>
openUrl(`https://modrinth.com/report?item=project&itemID=${data.value.id}`),
},
],
},
]
}
return [
{
id: 'install',
label: installButtonLabel.value,
icon:
installButtonLoading.value && !installButtonInstalled.value
? SpinnerIcon
: !installButtonInstalled.value && !serverProjectSelected.value
? DownloadIcon
: CheckIcon,
iconClass:
installButtonLoading.value && !installButtonInstalled.value ? 'animate-spin' : undefined,
color: 'brand',
tooltip: installButtonTooltip.value,
disabled: installButtonDisabled.value,
onClick: () => install(null),
},
{
id: 'more',
label: 'More options',
icon: MoreVerticalIcon,
labelHidden: true,
type: 'transparent',
tooltip: 'More options',
menuActions: [
{
id: 'follow',
label: 'Follow',
icon: HeartIcon,
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'save',
label: 'Save',
icon: BookmarkIcon,
disabled: true,
tooltip: 'Coming soon',
action: () => {},
},
{
id: 'open-in-browser',
label: 'Open in browser',
icon: ExternalIcon,
action: () =>
openUrl(`https://modrinth.com/${data.value.project_type}/${data.value.slug}`),
},
{
divider: true,
},
{
id: 'report',
label: 'Report',
icon: ReportIcon,
color: 'red',
action: () => openUrl(`https://modrinth.com/report?item=project&itemID=${data.value.id}`),
},
],
},
]
})
const [allLoaders, allGameVersions] = await Promise.all([
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
@@ -587,10 +612,19 @@ async function fetchProjectData() {
function fetchDeferredServerData(project) {
const serverAddress = projectV3.value?.minecraft_java_server?.address
if (serverAddress) {
serverPing.value = undefined
getServerLatency(serverAddress)
.then((latency) => {
serverPing.value = latency
const cachedStatus = getFreshCachedServerStatus(queryClient, serverAddress)
if (cachedStatus) {
serverPing.value = cachedStatus.ping
serverStatusOnline.value = true
} else {
serverPing.value = undefined
}
fetchCachedServerStatus(queryClient, serverAddress)
.then((status) => {
if (projectV3.value?.minecraft_java_server?.address !== serverAddress) return
serverPing.value = status.ping
serverStatusOnline.value = true
})
.catch((error) => {
console.error(`Failed to ping server ${serverAddress}:`, error)
@@ -8,6 +8,7 @@ export interface InstanceSettingsContext {
offline?: boolean
isMinecraftServer: Ref<boolean>
onUnlinked: () => void
closeModal?: () => void
}
export const [injectInstanceSettings, provideInstanceSettings] =
@@ -17,7 +17,19 @@ import {
writePendingServerContentInstallBaseline,
writeStoredServerInstallQueue,
} from '@modrinth/ui'
import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue'
import { useQueryClient } from '@tanstack/vue-query'
import {
computed,
type ComputedRef,
nextTick,
onActivated,
onDeactivated,
type Ref,
ref,
shallowRef,
watch,
} from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
type ServerFlowFrom = 'onboarding' | 'reset-server'
@@ -52,6 +64,7 @@ export interface ServerInstallContentContext {
isSetupServerContext: ComputedRef<boolean>
effectiveServerWorldId: ComputedRef<string | null>
serverContextServerData: Ref<Archon.Servers.v0.Server | null>
serverContextWorldName: ComputedRef<string | null>
serverContentProjectIds: Ref<Set<string>>
queuedServerInstallProjectIds: ComputedRef<Set<string>>
queuedServerInstallCount: ComputedRef<number>
@@ -202,16 +215,31 @@ async function getQueuedInstallPlaceholders(
export function createServerInstallContent(opts: {
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
isRouteInContext?: (route: RouteLocationNormalizedLoaded) => boolean
}) {
const { serverSetupModalRef } = opts
const route = useRoute()
const router = useRouter()
const client = injectModrinthClient()
const { handleError } = injectNotificationManager()
const queryClient = useQueryClient()
const serverIdQuery = computed(() => readQueryString(route.query.sid))
const worldIdQuery = computed(() => readQueryString(route.query.wid))
const browseFrom = computed(() => readQueryString(route.query.from))
const routeInContext = computed(() => opts.isRouteInContext?.(route) ?? true)
const contextQuery = shallowRef(route.query)
watch(
[() => route.fullPath, routeInContext],
() => {
if (routeInContext.value) {
contextQuery.value = route.query
}
},
{ immediate: true },
)
const serverIdQuery = computed(() => readQueryString(contextQuery.value.sid))
const worldIdQuery = computed(() => readQueryString(contextQuery.value.wid))
const browseFrom = computed(() => readQueryString(contextQuery.value.from))
const serverFlowFrom = computed<ServerFlowFrom | null>(() =>
browseFrom.value === 'onboarding' || browseFrom.value === 'reset-server'
? browseFrom.value
@@ -224,11 +252,13 @@ export function createServerInstallContent(opts: {
const serverContextWorldId = ref<string | null>(worldIdQuery.value)
const serverContextServerData = ref<Archon.Servers.v0.Server | null>(null)
const serverContextServerFull = ref<Archon.Servers.v1.ServerFull | null>(null)
const serverContentProjectIds = ref<Set<string>>(new Set())
const serverContentInstallKeys = ref<Set<string>>(new Set())
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
new Map(),
)
const componentActive = ref(true)
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
const selectedServerInstallProjects = computed<BrowseSelectedProject[]>(() =>
@@ -241,6 +271,18 @@ export function createServerInstallContent(opts: {
const isInstallingQueuedServerInstalls = ref(false)
const queuedInstallProgress = ref({ completed: 0, total: 0 })
const effectiveServerWorldId = computed(() => worldIdQuery.value ?? serverContextWorldId.value)
const serverContextWorld = computed(() => {
const serverFull = serverContextServerFull.value
if (!serverFull) return null
const worldId = effectiveServerWorldId.value
if (worldId) {
return serverFull.worlds.find((world) => world.id === worldId) ?? null
}
return serverFull.worlds.find((world) => world.is_active) ?? serverFull.worlds[0] ?? null
})
const serverContextWorldName = computed(() => serverContextWorld.value?.name ?? null)
const serverBackUrl = computed(() => {
const sid = serverIdQuery.value
if (!sid) return '/hosting/manage'
@@ -250,7 +292,7 @@ export function createServerInstallContent(opts: {
if (serverFlowFrom.value === 'reset-server') {
return `/hosting/manage/${sid}?openSettings=installation`
}
return `/hosting/manage/${sid}/content`
return getServerInstanceContentPath(sid, effectiveServerWorldId.value)
})
const serverBackLabel = computed(() => {
if (serverFlowFrom.value === 'onboarding') return 'Back to setup'
@@ -264,9 +306,27 @@ export function createServerInstallContent(opts: {
return 'Installing content'
})
onActivated(() => {
componentActive.value = true
})
onDeactivated(() => {
componentActive.value = false
})
async function getServerContextServerFull(serverId: string) {
if (serverContextServerFull.value?.id === serverId) {
return serverContextServerFull.value
}
const server = await client.archon.servers_v1.get(serverId)
serverContextServerFull.value = server
return server
}
async function resolveServerContextWorldId(serverId: string) {
try {
const server = await client.archon.servers_v1.get(serverId)
const server = await getServerContextServerFull(serverId)
const activeWorld = server.worlds.find((world) => world.is_active)
return activeWorld?.id ?? server.worlds[0]?.id ?? null
} catch (err) {
@@ -302,6 +362,11 @@ export function createServerInstallContent(opts: {
} catch (err) {
handleError(err as Error)
}
try {
await getServerContextServerFull(sid)
} catch (err) {
handleError(err as Error)
}
let resolvedWorldId = effectiveServerWorldId.value
if (!resolvedWorldId) {
@@ -318,41 +383,79 @@ export function createServerInstallContent(opts: {
}
function watchServerContextChanges() {
watch([serverIdQuery, effectiveServerWorldId], async ([sid, wid], [prevSid, prevWid]) => {
if (!sid) {
serverContextServerData.value = null
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
setQueuedServerInstallPlans(new Map())
return
}
watch(
[componentActive, routeInContext, serverIdQuery, effectiveServerWorldId],
async ([active, inContext, sid, wid], [prevActive, prevInContext, prevSid, prevWid]) => {
if (!active || !inContext) return
if (sid !== prevSid) {
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
try {
serverContextServerData.value = await client.archon.servers_v0.get(sid)
} catch (err) {
handleError(err as Error)
if (!sid) {
serverContextServerData.value = null
serverContextServerFull.value = null
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
setQueuedServerInstallPlans(new Map())
return
}
}
if (wid !== prevWid) {
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
}
const hasServerDataForRoute = serverContextServerData.value?.server_id === sid
const hasServerFullForRoute = serverContextServerFull.value?.id === sid
const didEnterContext = !prevActive || !prevInContext
const shouldReloadRouteContext =
didEnterContext ||
sid !== prevSid ||
wid !== prevWid ||
!hasServerDataForRoute ||
!hasServerFullForRoute
if (wid && (sid !== prevSid || wid !== prevWid)) {
await refreshServerInstalledContent(sid, wid)
}
})
if (!hasServerDataForRoute || !hasServerFullForRoute) {
serverContextWorldId.value = worldIdQuery.value
if (!hasServerDataForRoute) {
serverContextServerData.value = null
}
if (!hasServerFullForRoute) {
serverContextServerFull.value = null
}
serverContentProjectIds.value = new Set()
serverContentInstallKeys.value = new Set()
}
if (!hasServerDataForRoute) {
try {
serverContextServerData.value = await client.archon.servers_v0.get(sid)
} catch (err) {
handleError(err as Error)
}
}
if (!hasServerFullForRoute) {
try {
const serverFull = await getServerContextServerFull(sid)
if (!worldIdQuery.value) {
const activeWorld = serverFull.worlds.find((world) => world.is_active)
serverContextWorldId.value = activeWorld?.id ?? serverFull.worlds[0]?.id ?? null
}
} catch (err) {
handleError(err as Error)
}
}
if (shouldReloadRouteContext) {
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
}
if (wid && shouldReloadRouteContext) {
await refreshServerInstalledContent(sid, wid)
}
},
)
}
function enforceSetupModpackRoute(currentProjectType: string | undefined) {
if (!isSetupServerContext.value || currentProjectType === 'modpack') return
if (!routeInContext.value || !isSetupServerContext.value || currentProjectType === 'modpack')
return
router.replace({
path: '/browse/modpack',
query: route.query,
query: contextQuery.value,
})
}
@@ -422,7 +525,7 @@ export function createServerInstallContent(opts: {
if (isInstallingQueuedServerInstalls.value) return false
if (!serverId || !worldId) {
handleError(new Error('No server world is available for install.'))
handleError(new Error('No server instance is available for install.'))
return false
}
@@ -471,6 +574,9 @@ export function createServerInstallContent(opts: {
...serverContentInstallKeys.value,
...result.flushedPlans.map((plan) => plan.projectId),
])
if (result.flushedPlans.length > 0) {
await queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] })
}
return true
} finally {
@@ -491,6 +597,7 @@ export function createServerInstallContent(opts: {
const plans = new Map(queuedServerInstalls.value)
if (sid && wid) {
writeStoredServerInstallQueue(sid, wid, plans)
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
void getQueuedInstallPlaceholders(client, plans)
@@ -550,7 +657,7 @@ export function createServerInstallContent(opts: {
if (serverFlowFrom.value === 'onboarding') {
await client.archon.servers_v1.endIntro(sid)
await router.push(`/hosting/manage/${sid}/content`)
await router.push(getServerInstanceContentPath(sid, wid))
return
}
@@ -565,6 +672,11 @@ export function createServerInstallContent(opts: {
serverContentProjectIds.value = new Set([...serverContentProjectIds.value, id])
}
function getServerInstanceContentPath(serverId: string, worldId: string | null) {
const base = `/hosting/manage/${encodeURIComponent(serverId)}/instances`
return worldId ? `${base}/${encodeURIComponent(worldId)}` : base
}
return {
serverIdQuery,
worldIdQuery,
@@ -575,6 +687,7 @@ export function createServerInstallContent(opts: {
isSetupServerContext,
effectiveServerWorldId,
serverContextServerData,
serverContextWorldName,
serverContentProjectIds,
queuedServerInstallProjectIds,
queuedServerInstallCount,
+58
View File
@@ -6,6 +6,11 @@ import * as Instance from '@/pages/instance'
import * as Library from '@/pages/library'
import * as Project from '@/pages/project'
function getQueryParam(value) {
if (Array.isArray(value)) return value[0] ?? ''
return value ?? ''
}
/**
* Configures application routing. Add page to pages/index and then add to route table here.
*/
@@ -57,6 +62,48 @@ export default new createRouter({
breadcrumb: [{ name: '?Server' }],
},
},
{
path: 'instances',
name: 'ServerManageInstances',
component: Hosting.Instances,
meta: {
breadcrumb: [{ name: '?Server' }],
},
},
{
path: 'instances/:instance_id',
name: 'ServerManageInstance',
component: Hosting.Instance,
meta: {
breadcrumb: [{ name: '?Server' }],
},
children: [
{
path: '',
name: 'ServerManageInstanceContent',
component: Hosting.Content,
meta: {
breadcrumb: [{ name: '?Server' }],
},
},
{
path: 'files',
name: 'ServerManageInstanceFiles',
component: Hosting.Files,
meta: {
breadcrumb: [{ name: '?Server' }],
},
},
{
path: 'backups',
name: 'ServerManageInstanceBackups',
component: Hosting.Backups,
meta: {
breadcrumb: [{ name: '?Server' }],
},
},
],
},
{
path: 'files',
name: 'ServerManageFiles',
@@ -81,6 +128,17 @@ export default new createRouter({
component: Pages.Browse,
meta: {
useContext: true,
keepAliveComponent: 'Browse',
keepAliveKey: (route) => {
return [
'browse',
getQueryParam(route.params.projectType),
getQueryParam(route.query.i),
getQueryParam(route.query.sid),
getQueryParam(route.query.wid),
getQueryParam(route.query.from),
].join(':')
},
breadcrumb: [{ name: '?BrowseTitle' }],
},
},
+1
View File
@@ -19,6 +19,7 @@
"types": ["vite/client"],
"paths": {
"@modrinth/api-client": ["../../packages/api-client/src/index.ts"],
"@/*": ["./src/*"]
}
},
+5
View File
@@ -8,6 +8,7 @@ import tauriConf from '../app/tauri.conf.json'
const projectRootDir = resolve(__dirname)
const appLibEnvDir = resolve(projectRootDir, '../../packages/app-lib')
const apiClientSource = resolve(projectRootDir, '../../packages/api-client/src/index.ts')
// Load .env from app-lib manually instead of using Vite's envDir, which would auto-load .env.local and override values
const envFilePath = resolve(appLibEnvDir, '.env')
@@ -37,6 +38,10 @@ export default defineConfig({
},
resolve: {
alias: [
{
find: '@modrinth/api-client',
replacement: apiClientSource,
},
{
find: '@',
replacement: resolve(projectRootDir, 'src'),
+13
View File
@@ -51,8 +51,21 @@ tauri-build = { workspace = true, features = ["codegen"] }
[target.'cfg(target_os = "linux")'.dependencies]
tauri-plugin-updater = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-graphics.workspace = true
objc2-app-kit = { workspace = true, features = [
"NSWindow",
] }
[target.'cfg(windows)'.dependencies]
webview2-com.workspace = true
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_UI_WindowsAndMessaging",
] }
windows-core.workspace = true
[features]
+127 -17
View File
@@ -11,13 +11,22 @@ use tokio::sync::RwLock;
pub struct AdsState {
pub shown: bool,
pub modal_shown: bool,
pub occluded: bool,
pub last_click: Option<Instant>,
pub malicious_origins: HashSet<String>,
}
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
#[cfg(any(windows, target_os = "macos"))]
pub(super) const OCCLUDED_AREA_THRESHOLD: f64 = 0.5;
#[cfg(not(target_os = "linux"))]
const ADS_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36";
const ADS_USER_AGENT: &str = concat!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ",
"(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ",
"ModrinthApp/",
env!("CARGO_PKG_VERSION"),
" (Modrinth App)",
);
#[cfg(windows)]
fn ads_user_agent_override_params() -> String {
@@ -28,12 +37,14 @@ fn ads_user_agent_override_params() -> String {
"brands": [
{ "brand": "Chromium", "version": "128" },
{ "brand": "Google Chrome", "version": "128" },
{ "brand": "Modrinth App", "version": env!("CARGO_PKG_VERSION") },
{ "brand": "Not=A?Brand", "version": "99" },
],
"fullVersion": "128.0.0.0",
"fullVersionList": [
{ "brand": "Chromium", "version": "128.0.0.0" },
{ "brand": "Google Chrome", "version": "128.0.0.0" },
{ "brand": "Modrinth App", "version": env!("CARGO_PKG_VERSION") },
{ "brand": "Not=A?Brand", "version": "99.0.0.0" },
],
"platform": "Windows",
@@ -52,8 +63,8 @@ fn configure_ads_cookie_settings(
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
) {
use webview2_com::Microsoft::Web::WebView2::Win32::{
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
ICoreWebView2_13, ICoreWebView2Profile3,
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2_13,
ICoreWebView2Profile3,
};
use windows_core::Interface;
@@ -83,10 +94,12 @@ fn configure_ads_cookie_settings(
}
}
fn set_webview_visible<R: Runtime>(
webview: &tauri::Webview<R>,
_visible: bool,
) {
fn set_webview_visible<R: Runtime>(webview: &tauri::Webview<R>, visible: bool) {
#[cfg(not(any(windows, target_os = "macos")))]
{
_ = visible;
}
webview
.with_webview(
#[allow(unused_variables)]
@@ -94,11 +107,18 @@ fn set_webview_visible<R: Runtime>(
#[cfg(windows)]
{
let controller = wv.controller();
unsafe { controller.SetIsVisible(_visible) }.ok();
unsafe { controller.SetIsVisible(visible) }.ok();
}
},
)
.ok();
#[cfg(target_os = "macos")]
if visible {
webview.show().ok();
} else {
webview.hide().ok();
}
}
fn set_webview_visible_for_window<R: Runtime>(
@@ -111,7 +131,76 @@ fn set_webview_visible_for_window<R: Runtime>(
.and_then(|window| window.is_minimized().ok())
.unwrap_or(false);
set_webview_visible(webview, visible && !is_minimized);
let is_occluded = app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| state.occluded)
.unwrap_or(false);
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
}
#[cfg(any(windows, target_os = "macos"))]
fn compute_ads_webview_occlusion<R: Runtime>(
app: &tauri::AppHandle<R>,
) -> Option<bool> {
let main_window = app.get_window("main")?;
let webviews = app.webviews();
let webview = webviews.get("ads-window")?;
#[cfg(target_os = "macos")]
{
let position = webview.position().ok()?;
let size = webview.size().ok()?;
Some(
crate::api::ads_occlusion_macos::is_ads_webview_occluded(
&main_window,
position.x,
position.y,
size.width,
size.height,
)
.unwrap_or(false),
)
}
#[cfg(windows)]
{
let position = webview.position().ok()?;
let size = webview.size().ok()?;
let hwnd = main_window.hwnd().ok()?;
Some(crate::api::ads_occlusion_windows::is_ads_webview_occluded(
hwnd,
position.x,
position.y,
size.width,
size.height,
))
}
}
#[cfg(any(windows, target_os = "macos"))]
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
let Some(occluded) = compute_ads_webview_occlusion(app) else {
return;
};
let state = app.state::<RwLock<AdsState>>();
let mut state = state.write().await;
if state.occluded == occluded {
return;
}
state.occluded = occluded;
let visible = state.shown && !state.modal_shown;
drop(state);
if let Some(webview) = app.webviews().get("ads-window") {
set_webview_visible_for_window(app, webview, visible);
}
}
fn sync_webview_visibility_for_main_window<R: Runtime>(
@@ -132,7 +221,7 @@ fn sync_webview_visibility_for_main_window<R: Runtime>(
false
} else {
match app.state::<RwLock<AdsState>>().try_read() {
Ok(state) => state.shown && !state.modal_shown,
Ok(state) => state.shown && !state.modal_shown && !state.occluded,
Err(_) => false,
}
};
@@ -165,20 +254,28 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
app.manage(RwLock::new(AdsState {
shown: true,
modal_shown: false,
occluded: false,
last_click: None,
malicious_origins: HashSet::new(),
}));
// We refresh the ads window every 5 minutes to mitigate memory leak issues.
// While this loop doesn't include explicit checks to see if the window is still
// visible when we refresh, the Aditude wrapper will not make any ad requests
// unless Chromium reports the page as visible. The refresh does not reset the
// visibility state.
// We refresh the ads window periodically to mitigate memory leak issues.
// Skip refreshes when app state has hidden the ads WebView. The refresh does
// not reset the visibility state.
let refresh_app = app.clone();
tauri::async_runtime::spawn(async move {
loop {
if let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
let should_refresh = refresh_app
.state::<RwLock<AdsState>>()
.try_read()
.map(|state| {
state.shown && !state.modal_shown && !state.occluded
})
.unwrap_or(false);
if should_refresh
&& let Some(webview) =
refresh_app.webviews().get_mut("ads-window")
{
let _ = webview.navigate(AD_LINK.parse().unwrap());
}
@@ -216,6 +313,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
});
}
#[cfg(any(windows, target_os = "macos"))]
{
let app_handle = app.clone();
tauri::async_runtime::spawn(async move {
loop {
sync_ads_occlusion(&app_handle).await;
tokio::time::sleep(Duration::from_millis(200)).await;
}
});
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
+236
View File
@@ -0,0 +1,236 @@
use std::ptr::NonNull;
use core_foundation::array::CFArray;
use core_foundation::base::{CFType, TCFType};
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_graphics::display::CGDisplay;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_graphics::window::{
CGWindowID, kCGWindowAlpha, kCGWindowBounds, kCGWindowLayer,
kCGWindowListExcludeDesktopElements,
kCGWindowListOptionOnScreenAboveWindow, kCGWindowListOptionOnScreenOnly,
kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID,
};
use objc2_app_kit::NSWindow;
pub fn is_ads_webview_occluded<R: tauri::Runtime>(
main_window: &tauri::Window<R>,
ad_x: i32,
ad_y: i32,
ad_width: u32,
ad_height: u32,
) -> Option<bool> {
let scale_factor = main_window.scale_factor().ok()?;
let maybe_ns_window = main_window.ns_window().ok()?;
let ns_window = NonNull::new(maybe_ns_window)?.cast::<NSWindow>();
let ns_window = unsafe { ns_window.as_ref() };
let window_number = ns_window.windowNumber();
let main_window_id = if window_number > 0 {
window_number as CGWindowID
} else {
return None;
};
let window_infos = CGDisplay::window_list_info(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
None,
)?;
let window_infos = unsafe {
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
window_infos.as_concrete_TypeRef(),
)
};
let main_window_rect = window_infos
.iter()
.find(|window_info| window_id(window_info) == Some(main_window_id))
.and_then(|window_info| window_rect(&window_info))?;
let ad_rect = ad_rect_from_main_window(
&main_window_rect,
ad_x,
ad_y,
ad_width,
ad_height,
scale_factor,
);
if is_empty_rect(&ad_rect) {
return None;
}
let ad_area = rect_area(&ad_rect);
if ad_area == 0.0 {
return None;
}
let app_process_id = std::process::id() as i32;
let windows_above_main = CGDisplay::window_list_info(
kCGWindowListOptionOnScreenAboveWindow
| kCGWindowListExcludeDesktopElements,
Some(main_window_id),
)?;
let windows_above_main = unsafe {
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
windows_above_main.as_concrete_TypeRef(),
)
};
let mut occluded_area = 0.0;
for window_info in windows_above_main.iter() {
if window_id(&window_info) == Some(main_window_id) {
continue;
}
if window_process_id(&window_info) == Some(app_process_id) {
continue;
}
let owner_name = window_owner_name(&window_info);
if owner_name.as_deref().is_some_and(is_system_window_owner) {
continue;
}
let layer = window_layer(&window_info);
if layer != Some(0) {
continue;
}
let alpha = window_alpha(&window_info);
if alpha.is_some_and(|alpha| alpha <= 0.0) {
continue;
}
let Some(rect) = window_rect(&window_info) else {
continue;
};
let Some(intersection) = intersect_rects(&ad_rect, &rect) else {
continue;
};
occluded_area += rect_area(&intersection);
let occluded_ratio = occluded_area / ad_area;
if occluded_ratio >= super::ads::OCCLUDED_AREA_THRESHOLD {
return Some(true);
}
}
Some(false)
}
fn ad_rect_from_main_window(
main_window_rect: &CGRect,
x: i32,
y: i32,
width: u32,
height: u32,
scale_factor: f64,
) -> CGRect {
let scale_factor = if scale_factor > 0.0 {
scale_factor
} else {
1.0
};
CGRect::new(
&CGPoint::new(
main_window_rect.origin.x + x as f64 / scale_factor,
main_window_rect.origin.y + y as f64 / scale_factor,
),
&CGSize::new(width as f64 / scale_factor, height as f64 / scale_factor),
)
}
fn window_id(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<CGWindowID> {
number_value(window_info, unsafe { kCGWindowNumber })
.and_then(|value| u32::try_from(value).ok())
}
fn window_process_id(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<i32> {
number_value(window_info, unsafe { kCGWindowOwnerPID })
.and_then(|value| i32::try_from(value).ok())
}
fn window_layer(window_info: &CFDictionary<CFString, CFType>) -> Option<i32> {
number_value(window_info, unsafe { kCGWindowLayer })
.and_then(|value| i32::try_from(value).ok())
}
fn window_alpha(window_info: &CFDictionary<CFString, CFType>) -> Option<f64> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowAlpha) };
window_info.find(&key)?.downcast::<CFNumber>()?.to_f64()
}
fn window_owner_name(
window_info: &CFDictionary<CFString, CFType>,
) -> Option<String> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowOwnerName) };
Some(window_info.find(&key)?.downcast::<CFString>()?.to_string())
}
fn is_system_window_owner(owner_name: &str) -> bool {
matches!(owner_name, "WindowManager")
}
fn number_value(
window_info: &CFDictionary<CFString, CFType>,
key: core_foundation::string::CFStringRef,
) -> Option<i64> {
let key = unsafe { CFString::wrap_under_get_rule(key) };
window_info.find(&key)?.downcast::<CFNumber>()?.to_i64()
}
fn window_rect(window_info: &CFDictionary<CFString, CFType>) -> Option<CGRect> {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowBounds) };
let bounds = window_info.find(&key)?.downcast::<CFDictionary>()?;
let rect = CGRect::from_dict_representation(&bounds)?;
if is_empty_rect(&rect) {
None
} else {
Some(rect)
}
}
fn is_empty_rect(rect: &CGRect) -> bool {
rect.size.width <= 0.0 || rect.size.height <= 0.0
}
fn rect_area(rect: &CGRect) -> f64 {
if is_empty_rect(rect) {
return 0.0;
}
rect.size.width * rect.size.height
}
fn intersect_rects(a: &CGRect, b: &CGRect) -> Option<CGRect> {
let left = a.origin.x.max(b.origin.x);
let top = a.origin.y.max(b.origin.y);
let right = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
let bottom = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
let rect = CGRect::new(
&CGPoint::new(left, top),
&CGSize::new(right - left, bottom - top),
);
if is_empty_rect(&rect) {
None
} else {
Some(rect)
}
}
+191
View File
@@ -0,0 +1,191 @@
use windows::Win32::Foundation::{HWND, POINT, RECT};
use windows::Win32::Graphics::Dwm::{
DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute,
};
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::UI::WindowsAndMessaging::{
GA_ROOT, GW_HWNDNEXT, GetAncestor, GetTopWindow, GetWindow, GetWindowRect,
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
};
pub fn is_ads_webview_occluded(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> bool {
let Some(ad_rect) = ad_rect_in_screen(main_hwnd, x, y, width, height)
else {
return false;
};
if is_empty_rect(&ad_rect) {
return false;
}
let ad_area = rect_area(&ad_rect);
if ad_area == 0 {
return false;
}
let mut occluded_area = 0u64;
let app_root = unsafe { GetAncestor(main_hwnd, GA_ROOT) };
let app_process_id = std::process::id();
let mut hwnd = match unsafe { GetTopWindow(None) } {
Ok(hwnd) => hwnd,
Err(_) => return false,
};
while !hwnd.is_invalid() {
let window_root = unsafe { GetAncestor(hwnd, GA_ROOT) };
if window_root == app_root {
return false;
}
if window_process_id(hwnd) == Some(app_process_id) {
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
continue;
}
if window_counts_as_occluder(hwnd)
&& let Some(occluder_rect) = window_rect(hwnd)
&& let Some(intersection) =
intersect_rects(&ad_rect, &occluder_rect)
{
occluded_area =
occluded_area.saturating_add(rect_area(&intersection));
if (occluded_area as f64 / ad_area as f64)
>= super::ads::OCCLUDED_AREA_THRESHOLD
{
return true;
}
}
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
Ok(hwnd) => hwnd,
Err(_) => break,
};
}
false
}
fn ad_rect_in_screen(
main_hwnd: HWND,
x: i32,
y: i32,
width: u32,
height: u32,
) -> Option<RECT> {
let mut origin = POINT { x: 0, y: 0 };
if !unsafe { ClientToScreen(main_hwnd, &mut origin).as_bool() } {
return None;
}
let left = origin.x.saturating_add(x);
let top = origin.y.saturating_add(y);
let right = left.saturating_add(width as i32);
let bottom = top.saturating_add(height as i32);
Some(RECT {
left,
top,
right,
bottom,
})
}
fn window_counts_as_occluder(hwnd: HWND) -> bool {
if !unsafe { IsWindowVisible(hwnd).as_bool() } {
return false;
}
if unsafe { IsIconic(hwnd).as_bool() } {
return false;
}
if is_dwm_cloaked(hwnd) {
return false;
}
true
}
fn window_process_id(hwnd: HWND) -> Option<u32> {
let mut process_id = 0u32;
unsafe {
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
}
(process_id != 0).then_some(process_id)
}
fn is_dwm_cloaked(hwnd: HWND) -> bool {
let mut cloaked = 0u32;
unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_CLOAKED,
&mut cloaked as *mut u32 as *mut _,
std::mem::size_of::<u32>() as u32,
)
}
.is_ok()
&& cloaked != 0
}
fn window_rect(hwnd: HWND) -> Option<RECT> {
let mut rect = RECT::default();
if unsafe {
DwmGetWindowAttribute(
hwnd,
DWMWA_EXTENDED_FRAME_BOUNDS,
&mut rect as *mut RECT as *mut _,
std::mem::size_of::<RECT>() as u32,
)
}
.is_err()
&& unsafe { GetWindowRect(hwnd, &mut rect) }.is_err()
{
return None;
}
if is_empty_rect(&rect) {
return None;
}
Some(rect)
}
fn is_empty_rect(rect: &RECT) -> bool {
rect.right <= rect.left || rect.bottom <= rect.top
}
fn rect_area(rect: &RECT) -> u64 {
if is_empty_rect(rect) {
return 0;
}
(rect.right - rect.left) as u64 * (rect.bottom - rect.top) as u64
}
fn intersect_rects(a: &RECT, b: &RECT) -> Option<RECT> {
let rect = RECT {
left: a.left.max(b.left),
top: a.top.max(b.top),
right: a.right.min(b.right),
bottom: a.bottom.min(b.bottom),
};
(!is_empty_rect(&rect)).then_some(rect)
}
+4
View File
@@ -18,6 +18,10 @@ pub mod tags;
pub mod utils;
pub mod ads;
#[cfg(target_os = "macos")]
mod ads_occlusion_macos;
#[cfg(windows)]
mod ads_occlusion_windows;
pub mod cache;
pub mod files;
pub mod friends;
+52 -14
View File
@@ -6,6 +6,7 @@
use native_dialog::{DialogBuilder, MessageLevel};
use std::env;
use std::sync::atomic::Ordering;
use tauri::{Listener, Manager};
use tauri_plugin_fs::FsExt;
use theseus::prelude::*;
@@ -96,6 +97,17 @@ fn restart_app(app: tauri::AppHandle) {
app.restart();
}
#[tauri::command]
async fn set_restart_after_pending_update(
should_restart: bool,
) -> api::Result<()> {
let state = State::get().await?;
state
.restart_after_pending_update
.store(should_restart, Ordering::Relaxed);
Ok(())
}
// if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks
fn main() {
@@ -245,6 +257,7 @@ fn main() {
get_update_size,
enqueue_update_for_installation,
remove_enqueued_update,
set_restart_after_pending_update,
toggle_decorations,
show_window,
restart_app,
@@ -262,7 +275,13 @@ fn main() {
#[cfg(feature = "updater")]
if matches!(event, tauri::RunEvent::Exit) {
let update_data = app.state::<PendingUpdateData>().inner();
if let Some((update, data)) = &*update_data.0.lock().unwrap() {
let should_restart = State::get_if_initialized()
.map(|s| {
s.restart_after_pending_update.load(Ordering::Relaxed)
})
.unwrap_or(false);
if let Some((update, data)) = &*update_data.0.lock().unwrap()
{
fn set_changelog_toast(version: Option<String>) {
let toast_result: theseus::Result<()> = tauri::async_runtime::block_on(async move {
let mut settings = settings::get().await?;
@@ -271,27 +290,46 @@ fn main() {
Ok(())
});
if let Err(e) = toast_result {
tracing::warn!("Failed to set pending_update_toast: {e}")
tracing::warn!(
"Failed to set pending_update_toast: {e}"
)
}
}
set_changelog_toast(Some(update.version.clone()));
if let Err(e) = update.install(data) {
tracing::error!("Error while updating: {e}");
set_changelog_toast(None);
match update.install(data) {
Ok(()) => {
if should_restart {
tracing::info!(
"Pending update installed successfully (version {}); restarting because user requested reload",
update.version
);
app.restart();
} else {
tracing::info!(
"Pending update installed successfully (version {}); exiting without relaunch (user did not request reload)",
update.version
);
}
}
Err(e) => {
tracing::error!(
"Pending update install failed (version {}): {e}",
update.version
);
set_changelog_toast(None);
DialogBuilder::message()
.set_level(MessageLevel::Error)
.set_title("Update error")
.set_text(format!("Failed to install update due to an error:\n{e}"))
.alert()
.show()
.unwrap();
DialogBuilder::message()
.set_level(MessageLevel::Error)
.set_title("Update error")
.set_text(format!("Failed to install update due to an error:\n{e}"))
.alert()
.show()
.unwrap();
}
}
app.restart();
}
}
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = event {
tracing::info!("Handling webview open {urls:?}");
+12 -1
View File
@@ -1,10 +1,15 @@
import { GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
import serverSidedVue from '@vitejs/plugin-vue'
import fs from 'fs/promises'
import { defineNuxtConfig } from 'nuxt/config'
import { fileURLToPath } from 'url'
import svgLoader from 'vite-svg-loader'
import { GenericModrinthClient, type Labrinth } from '../../packages/api-client/src/index.ts'
const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/'
const API_CLIENT_SOURCE = fileURLToPath(
new URL('../../packages/api-client/src/index.ts', import.meta.url),
)
const preloadedFonts = [
'inter/Inter-Regular.woff2',
@@ -24,6 +29,9 @@ const STAGING_MODRINTH_URL = 'https://staging.modrinth.com'
export default defineNuxtConfig({
srcDir: 'src/',
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
app: {
head: {
htmlAttrs: {
@@ -82,6 +90,9 @@ export default defineNuxtConfig({
},
cacheDir: '../../node_modules/.vite/apps/knossos',
resolve: {
alias: {
'@modrinth/api-client': API_CLIENT_SOURCE,
},
dedupe: ['vue'],
},
plugins: [
+1 -2
View File
@@ -38,7 +38,6 @@
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@intercom/messenger-js-sdk": "^0.0.14",
"@ltd/j-toml": "^1.38.0",
"@modrinth/api-client": "workspace:*",
"@modrinth/assets": "workspace:*",
@@ -47,7 +46,7 @@
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@sentry/nuxt": "^10.33.0",
"@tanstack/vue-query": "^5.90.7",
"@tanstack/vue-query": "5.90.7",
"@types/three": "^0.172.0",
"@vitejs/plugin-vue": "^6.0.3",
"@vue-email/components": "^0.0.21",
@@ -1,5 +1,12 @@
<script setup lang="ts">
import { BlueskyIcon, DiscordIcon, GithubIcon, MastodonIcon, TwitterIcon } from '@modrinth/assets'
import {
BlueskyIcon,
DiscordIcon,
GithubIcon,
MastodonIcon,
ToggleRightIcon,
TwitterIcon,
} from '@modrinth/assets'
import {
AutoLink,
ButtonStyled,
@@ -10,6 +17,7 @@ import {
type MessageDescriptor,
useVIntl,
} from '@modrinth/ui'
import { commonSettingsMessages } from '@modrinth/ui/src/utils/common-messages.js'
import TextLogo from '~/components/brand/TextLogo.vue'
@@ -233,11 +241,21 @@ function developerModeIncrement() {
role="region"
:aria-label="formatMessage(messages.modrinthInformation)"
>
<TextLogo
aria-hidden="true"
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
@click="developerModeIncrement()"
/>
<div class="flex items-center gap-2">
<TextLogo
aria-hidden="true"
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
@click="developerModeIncrement()"
/>
<ButtonStyled v-if="flags.developerMode" circular type="transparent" color="brand">
<nuxt-link
v-tooltip="formatMessage(commonSettingsMessages.featureFlags)"
to="/settings/flags"
>
<ToggleRightIcon />
</nuxt-link>
</ButtonStyled>
</div>
<div class="flex flex-wrap justify-center gap-px sm:-mx-2">
<ButtonStyled
v-for="(social, index) in socialLinks"
@@ -0,0 +1,127 @@
<template>
<ButtonStyled size="large" circular>
<PopoutMenu
v-if="authUser"
:tooltip="
saved ? formatMessage(commonMessages.savedLabel) : formatMessage(commonMessages.saveButton)
"
from="top-right"
:aria-label="formatMessage(commonMessages.saveButton)"
:dropdown-id="`${baseId}-save`"
>
<BookmarkIcon aria-hidden="true" :fill="saved ? 'currentColor' : 'none'" />
<template #menu>
<StyledInput
v-model="displayCollectionsSearch"
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
wrapper-class="menu-search"
/>
<div v-if="filteredCollections.length > 0" class="collections-list text-primary">
<Checkbox
v-for="option in filteredCollections"
:key="option.id"
:model-value="option.projects.includes(projectId)"
class="popout-checkbox"
@update:model-value="() => collectProject(option, projectId)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">{{ noCollectionsLabel }}</p>
</div>
<ButtonStyled>
<button class="mx-3 mb-3" @click="createCollection">
<PlusIcon aria-hidden="true" />
{{ createNewCollectionLabel }}
</button>
</ButtonStyled>
</template>
</PopoutMenu>
<nuxt-link
v-else
v-tooltip="formatMessage(commonMessages.saveButton)"
:to="signInRoute"
:aria-label="formatMessage(commonMessages.saveButton)"
>
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
</template>
<script setup lang="ts">
import { BookmarkIcon, PlusIcon } from '@modrinth/assets'
import {
ButtonStyled,
Checkbox,
commonMessages,
PopoutMenu,
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { computed, ref } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
type CollectionOption = {
id: string
name: string
projects: string[]
}
const props = defineProps<{
authUser?: unknown
signInRoute: RouteLocationRaw
projectId: string
collections: CollectionOption[]
saved: boolean
baseId: string
noCollectionsLabel: string
createNewCollectionLabel: string
collectProject: (option: CollectionOption, projectId: string) => void | Promise<void>
createCollection: (event: MouseEvent) => void
}>()
const { formatMessage } = useVIntl()
const displayCollectionsSearch = ref('')
const filteredCollections = computed(() =>
props.collections
.filter((collection) =>
collection.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
)
.slice()
.sort((a, b) => a.name.localeCompare(b.name)),
)
</script>
<style scoped lang="scss">
.popout-checkbox {
padding: var(--gap-sm) var(--gap-md);
white-space: nowrap;
&:hover {
filter: brightness(0.95);
}
}
.menu-text {
padding: 0 var(--gap-md);
font-size: var(--font-size-nm);
color: var(--color-secondary);
}
.menu-search {
margin: var(--gap-sm) var(--gap-md);
width: calc(100% - var(--gap-md) * 2);
}
.collections-list {
max-height: 40rem;
overflow-y: auto;
background-color: var(--color-bg);
border-radius: var(--radius-md);
margin: var(--gap-sm) var(--gap-md);
padding: var(--gap-sm);
}
</style>
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
import { XCircleIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const tempIgnored = ref(false)
const messages = defineMessages({
title: {
@@ -13,16 +17,34 @@ const messages = defineMessages({
defaultMessage:
"This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}",
},
ignoreErrors: {
id: 'layout.banner.build-fail.ignore',
defaultMessage: 'Ignore',
},
alwaysIgnore: {
id: 'layout.banner.build-fail.always-ignore',
defaultMessage: 'Always ignore',
},
})
defineProps<{
errors: any[] | undefined
apiUrl: string
}>()
function alwaysIgnoreBanner() {
flags.value.alwaysIgnoreErrorBanner = true
saveFeatureFlags()
}
</script>
<template>
<PagewideBanner v-if="errors?.length" variant="error">
<PagewideBanner
v-if="
flags.showAllBanners || (errors?.length && !tempIgnored && !flags.alwaysIgnoreErrorBanner)
"
variant="error"
>
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
@@ -34,5 +56,19 @@ defineProps<{
})
}}
</template>
<template #actions_right>
<ButtonStyled color="red" type="transparent" hover-color-fill="background">
<button @click="alwaysIgnoreBanner">
<XCircleIcon />
{{ formatMessage(messages.alwaysIgnore) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="tempIgnored = true">
<XIcon />
{{ formatMessage(messages.ignoreErrors) }}
</button>
</ButtonStyled>
</template>
</PagewideBanner>
</template>
@@ -13,6 +13,7 @@ import {
const { formatMessage } = useVIntl()
const flags = useFeatureFlags()
const config = useRuntimeConfig()
const route = useRoute()
const messages = defineMessages({
title: {
@@ -21,7 +22,7 @@ const messages = defineMessages({
},
description: {
id: 'layout.banner.preview.description',
defaultMessage: `If you meant to access the official Modrinth website, visit <link>https://modrinth.com</link>. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}.`,
defaultMessage: `If you meant to access the official Modrinth website, visit {url}. This preview deploy is used by Modrinth staff for testing purposes. It was built using <branch-link>{owner}/{branch}</branch-link> @ {commit}.`,
},
})
@@ -29,10 +30,12 @@ function hidePreviewBanner() {
flags.value.hidePreviewBanner = true
saveFeatureFlags()
}
const url = computed(() => `https://modrinth.com${route.fullPath}`)
</script>
<template>
<PagewideBanner v-if="!flags.hidePreviewBanner" variant="info">
<PagewideBanner v-if="!flags.hidePreviewBanner || flags.showAllBanners" variant="info">
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
@@ -45,9 +48,9 @@ function hidePreviewBanner() {
branch: config.public.branch,
}"
>
<template #link="{ children }">
<a href="https://modrinth.com" target="_blank" rel="noopener" class="text-link">
<component :is="() => normalizeChildren(children)" />
<template #url>
<a :href="url" target="_blank" rel="noopener" class="text-link">
{{ url }}
</a>
</template>
<template #branch-link="{ children }">
@@ -75,7 +78,7 @@ function hidePreviewBanner() {
</IntlFormatted>
</span>
</template>
<template #actions_right>
<template #actions_top_right>
<ButtonStyled type="transparent" circular>
<button :aria-label="formatMessage(commonMessages.closeButton)" @click="hidePreviewBanner">
<XIcon aria-hidden="true" />
@@ -47,7 +47,7 @@ function hideRussiaCensorshipBanner() {
<span class="text-xs font-medium">(Перевод на русский)</span>
</nuxt-link>
</ButtonStyled>
<ButtonStyled>
<ButtonStyled type="transparent" hover-color-fill="background">
<nuxt-link to="/news/article/standing-by-our-values">
<BookTextIcon /> Read our full statement
<span class="text-xs font-medium">(English)</span>
@@ -55,7 +55,7 @@ function hideRussiaCensorshipBanner() {
</ButtonStyled>
</div>
</template>
<template #actions_right>
<template #actions_top_right>
<ButtonStyled circular type="transparent">
<button
v-tooltip="formatMessage(commonMessages.closeButton)"
@@ -10,6 +10,7 @@ import {
const { formatMessage } = useVIntl()
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const messages = defineMessages({
title: {
@@ -29,14 +30,14 @@ function hideStagingBanner() {
</script>
<template>
<PagewideBanner v-if="!cosmetics.hideStagingBanner" variant="warning">
<PagewideBanner v-if="flags.showAllBanners || !cosmetics.hideStagingBanner" variant="warning">
<template #title>
<span>{{ formatMessage(messages.title) }}</span>
</template>
<template #description>
{{ formatMessage(messages.description) }}
</template>
<template #actions_right>
<template #actions_top_right>
<ButtonStyled type="transparent" circular>
<button :aria-label="formatMessage(commonMessages.closeButton)" @click="hideStagingBanner">
<XIcon aria-hidden="true" />
@@ -29,8 +29,8 @@ const messages = defineMessages({
<template #description>
<span>{{ formatMessage(messages.description) }}</span>
</template>
<template #actions>
<ButtonStyled>
<template #actions_right>
<ButtonStyled color="red">
<nuxt-link to="/settings/billing">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(messages.action) }}
@@ -55,7 +55,7 @@ function openTaxForm(e: MouseEvent) {
formatMessage(messages.description, { threshold: formatMoney(taxThreshold) })
}}</span>
</template>
<template #actions>
<template #actions_right>
<ButtonStyled color="orange">
<button @click="openTaxForm"><FileTextIcon /> {{ formatMessage(messages.action) }}</button>
</ButtonStyled>
@@ -29,7 +29,7 @@ const messages = defineMessages({
<template #description>
<span>{{ formatMessage(messages.description) }}</span>
</template>
<template #actions>
<template #actions_right>
<div class="flex w-fit flex-row">
<ButtonStyled color="red">
<nuxt-link to="https://support.modrinth.com" target="_blank" rel="noopener">
@@ -96,14 +96,12 @@ async function handleResendEmailVerification() {
}}
</span>
</template>
<template #actions>
<ButtonStyled v-if="hasEmail">
<button @click="handleResendEmailVerification">
<template #actions_right>
<ButtonStyled color="orange">
<button v-if="hasEmail" @click="handleResendEmailVerification">
{{ formatMessage(verifyEmailBannerMessages.action) }}
</button>
</ButtonStyled>
<ButtonStyled v-else>
<nuxt-link to="/settings/account">
<nuxt-link v-else to="/settings/account">
<SettingsIcon aria-hidden="true" />
{{ formatMessage(addEmailBannerMessages.action) }}
</nuxt-link>
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { PagewideBanner } from '@modrinth/ui'
const flags = useFeatureFlags()
const route = useRoute()
const url = computed(() => `https://modrinth.com${route.fullPath}`)
const bannerRoot = ref<HTMLElement | null>(null)
function onProdLinkClick(e: MouseEvent) {
e.preventDefault()
const el = bannerRoot.value
if (el) {
const { height } = el.getBoundingClientRect()
window.scrollBy({ top: Math.ceil(height), behavior: 'auto' })
}
window.open(url.value, '_blank', 'noopener,noreferrer')
}
</script>
<template>
<div v-if="flags.showViewProdRouteBanner || flags.showAllBanners" ref="bannerRoot">
<PagewideBanner variant="info" slim>
<template #description>
<span>
View route on production:
<a
:href="url"
target="_blank"
rel="noopener noreferrer"
class="text-link"
@click="onProdLinkClick"
>
{{ url }}
</a>
</span>
</template>
</PagewideBanner>
</div>
</template>
@@ -2,28 +2,29 @@
<div
class="flex h-11 items-center justify-between gap-2 rounded-xl bg-button-bg px-4 py-1 text-button-text"
>
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="flex min-w-0 flex-1 items-center justify-start gap-2">
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
<span v-tooltip="name || projectId" class="truncate font-semibold text-contrast">
<span
v-tooltip="name || projectId"
class="min-w-0 max-w-fit flex-1 truncate font-semibold text-contrast"
>
{{ name || 'Unknown Project' }}
</span>
<span
v-if="versionNumber"
v-tooltip="versionNumber"
class="min-w-0 max-w-fit flex-1 truncate whitespace-nowrap text-sm font-medium"
>
{{ versionNumber }}
</span>
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
{{ dependencyType }}
</TagItem>
</div>
<span
v-if="versionName"
v-tooltip="versionName"
class="truncate whitespace-nowrap font-medium"
:class="!hideRemove ? 'max-w-[35%]' : 'max-w-[50%]'"
>
{{ versionName }}
</span>
<div v-if="!hideRemove" class="flex items-center justify-end gap-1">
<div v-if="!hideRemove" class="flex shrink-0 items-center justify-end gap-1">
<ButtonStyled size="standard" :circular="true">
<button aria-label="Remove file" class="-mr-2 !shadow-none" @click="emitRemove">
<XIcon aria-hidden="true" />
@@ -43,12 +44,12 @@ const emit = defineEmits<{
(e: 'remove'): void
}>()
const { projectId, name, icon, dependencyType, versionName, hideRemove } = defineProps<{
const { projectId, name, icon, dependencyType, versionNumber, hideRemove } = defineProps<{
projectId: string
name?: string
icon?: string
dependencyType: Labrinth.Versions.v2.DependencyType
versionName?: string
versionNumber?: string
hideRemove?: boolean
}>()
@@ -8,7 +8,7 @@
:name="dependency.name"
:icon="dependency.icon"
:dependency-type="dependency.dependencyType"
:version-name="dependency.versionName"
:version-number="dependency.versionNumber"
:hide-remove="disableRemove"
@remove="() => removeDependency(index)"
/>
@@ -35,7 +35,7 @@ const addedDependencies = computed(() =>
if (!dep.project_id) return null
const dependencyProject = dependencyProjects.value[dep.project_id]
const versionName = dependencyVersions.value[dep.version_id || '']?.name ?? ''
const versionNumber = dependencyVersions.value[dep.version_id || '']?.version_number ?? ''
if (!dependencyProject && projectsFetchLoading.value) return null
@@ -44,7 +44,7 @@ const addedDependencies = computed(() =>
name: dependencyProject?.name,
icon: dependencyProject?.icon_url,
dependencyType: dep.dependency_type,
versionName,
versionNumber,
}
})
.filter(Boolean),
@@ -3,11 +3,16 @@
v-model="projectId"
placeholder="Select project"
:options="options"
:searchable="true"
:search-value="selectedProjectOption?.label"
search-placeholder="Search by name or paste ID..."
:no-options-message="searchLoading ? 'Loading...' : 'No results found'"
:disable-search-filter="true"
searchable
disable-search-filter
select-search-text-on-focus
:show-chevron="false"
@search-input="(query) => handleSearch(query)"
@search-blur="handleSearchBlur"
@select="handleSelect"
/>
</template>
@@ -15,15 +20,37 @@
import type { ComboboxOption } from '@modrinth/ui'
import { Combobox, injectModrinthClient, injectNotificationManager } from '@modrinth/ui'
import { useDebounceFn } from '@vueuse/core'
import { defineAsyncComponent, h } from 'vue'
import { defineAsyncComponent, h, markRaw, ref, watch } from 'vue'
const { addNotification } = injectNotificationManager()
const projectId = defineModel<string>()
const searchLoading = ref(false)
const options = ref<ComboboxOption<string>[]>([])
const selectedProjectOption = ref<ComboboxOption<string>>()
const selectedProjectSearchQuery = ref('')
const { labrinth } = injectModrinthClient()
let latestSearchQuery = ''
function hitToOption(hit: { title: string; project_id: string; icon_url?: string | null }) {
return {
label: hit.title,
value: hit.project_id,
icon: markRaw(
defineAsyncComponent(() =>
Promise.resolve({
setup: () => () =>
h('img', {
src: hit.icon_url,
alt: hit.title,
class: 'h-5 w-5 rounded',
}),
}),
),
),
}
}
const search = async (query: string) => {
query = query.trim()
@@ -53,36 +80,77 @@ const search = async (query: string) => {
facets: [[`project_id:${query.replace(/[^a-zA-Z0-9]/g, '')}`]], // remove any non-alphanumeric characters
})
options.value = [...resultsByProjectId.hits, ...results.hits].map((hit) => ({
label: hit.title,
value: hit.project_id,
icon: markRaw(
defineAsyncComponent(() =>
Promise.resolve({
setup: () => () =>
h('img', {
src: hit.icon_url,
alt: hit.title,
class: 'h-5 w-5 rounded',
}),
}),
),
),
}))
if (query !== latestSearchQuery) return
options.value = [...resultsByProjectId.hits, ...results.hits].map(hitToOption)
} catch (error: any) {
if (query !== latestSearchQuery) return
addNotification({
title: 'An error occurred',
text: error.data ? error.data.description : error,
type: 'error',
})
}
searchLoading.value = false
if (query === latestSearchQuery) {
searchLoading.value = false
}
}
const throttledSearch = useDebounceFn(search, 500)
const throttledSearch = useDebounceFn(search, 250)
const runSearch = async (query: string, debounce: boolean) => {
query = query.trim()
latestSearchQuery = query
if (!query) {
searchLoading.value = false
options.value = []
await throttledSearch(query)
return
}
searchLoading.value = true
await (debounce ? throttledSearch(query) : search(query))
}
const handleSearch = async (query: string) => {
searchLoading.value = true
await throttledSearch(query)
await runSearch(query, true)
}
const handleSelect = (option: ComboboxOption<string>) => {
selectedProjectOption.value = option
selectedProjectSearchQuery.value = latestSearchQuery
}
const handleSearchBlur = async () => {
if (!projectId.value) return
const selectedOption =
options.value.find((option) => option.value === projectId.value) ??
(selectedProjectOption.value?.value === projectId.value
? selectedProjectOption.value
: undefined)
if (!selectedOption) return
await runSearch(selectedProjectSearchQuery.value || selectedOption.label, false)
if (!options.value.some((option) => option.value === selectedOption.value)) {
options.value = [selectedOption, ...options.value]
}
}
watch(projectId, (value) => {
if (!value) {
selectedProjectOption.value = undefined
selectedProjectSearchQuery.value = ''
return
}
const option = options.value.find((option) => option.value === value)
if (option) {
selectedProjectOption.value = option
}
})
</script>
@@ -8,7 +8,7 @@
:name="dependency.name"
:icon="dependency.icon"
:dependency-type="dependency.dependency_type"
:version-name="dependency.versionName"
:version-number="dependency.versionNumber"
@on-add-suggestion="
() =>
handleAddSuggestion({
@@ -1,28 +1,31 @@
<template>
<div
class="flex items-center justify-between gap-2 rounded-xl border-2 border-dashed border-surface-5 px-4 py-1 text-button-text"
class="flex h-11 items-center justify-between gap-2 rounded-xl border-2 border-dashed border-surface-5 px-4 py-1 text-button-text"
>
<div class="grid max-w-[75%] grid-cols-[auto_1fr_auto] items-center gap-2">
<div class="flex min-w-0 flex-1 items-center justify-start gap-2">
<Avatar v-if="icon" :src="icon" alt="dependency-icon" size="20px" :no-shadow="true" />
<span v-tooltip="name || 'Unknown Project'" class="truncate font-semibold text-contrast">
<span
v-tooltip="name || 'Unknown Project'"
class="min-w-0 max-w-fit flex-1 truncate font-semibold text-contrast"
>
{{ name || 'Unknown Project' }}
</span>
<span
v-if="versionNumber"
v-tooltip="versionNumber"
class="min-w-0 max-w-fit flex-1 truncate whitespace-nowrap text-sm font-medium"
>
{{ versionNumber }}
</span>
<TagItem class="shrink-0 border !border-solid border-surface-5 capitalize">
{{ dependencyType }}
</TagItem>
</div>
<span
v-if="versionName"
v-tooltip="versionName"
class="max-w-[35%] truncate whitespace-nowrap font-medium"
>
{{ versionName }}
</span>
<div class="flex items-center justify-end gap-1">
<div class="flex shrink-0 items-center justify-end gap-1">
<ButtonStyled size="standard" :circular="true" type="transparent">
<button aria-label="Add dependency" class="!shadow-none" @click="emitAddSuggestion">
<PlusIcon aria-hidden="true" />
@@ -41,11 +44,11 @@ const emit = defineEmits<{
(e: 'onAddSuggestion'): void
}>()
const { name, icon, dependencyType, versionName } = defineProps<{
const { name, icon, dependencyType, versionNumber } = defineProps<{
name?: string
icon?: string
dependencyType: Labrinth.Versions.v2.DependencyType
versionName?: string
versionNumber?: string
}>()
function emitAddSuggestion() {
@@ -1,11 +1,10 @@
<template>
<NavTabs
<Tabs
v-if="editingVersion"
mode="local"
:links="editTabLinks"
:active-index="2"
class="mb-4 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
value="add-files"
:tabs="editTabs"
class="mb-5 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
/>
<div class="flex w-full flex-col gap-4">
<template
@@ -99,7 +98,8 @@ import {
defineMessages,
DropzoneFileInput,
injectProjectPageContext,
NavTabs,
Tabs,
type TabsTab,
useVIntl,
} from '@modrinth/ui'
import { acceptFileFromProjectType } from '@modrinth/utils'
@@ -124,18 +124,14 @@ const {
handleNewFiles,
} = injectManageVersionContext()
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
const editTabLinks = editTabs.map(({ label, href }) => ({ label, href }))
function setEditTab(index: number) {
const tab = editTabs[index]
if (!tab) return
modal.value?.setStage(tab.stage)
function setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
}
function handleRemoveFile(index: number) {
@@ -1,182 +1,58 @@
<template>
<div class="flex w-full max-w-full flex-col gap-6">
<div class="flex flex-col gap-4">
<span class="font-semibold text-contrast">Add dependency</span>
<div class="flex flex-col gap-3 rounded-2xl border border-solid border-surface-5 p-4">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast">Project</span>
<DependencySelect v-model="newDependencyProjectId" />
</div>
<div class="flex w-full max-w-full flex-col gap-3">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast">Project</span>
<DependencySelect v-model="newDependencyProjectId" />
</div>
<template v-if="newDependencyProjectId">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Version </span>
<Combobox
v-model="newDependencyVersionId"
placeholder="Select version"
:options="[{ label: 'Any version', value: null }, ...newDependencyVersions]"
:searchable="true"
/>
</div>
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Dependency relation </span>
<Combobox
v-model="newDependencyType"
placeholder="Select dependency type"
:options="[
{ label: 'Required', value: 'required' },
{ label: 'Optional', value: 'optional' },
{ label: 'Incompatible', value: 'incompatible' },
{ label: 'Embedded', value: 'embedded' },
]"
/>
</div>
<ButtonStyled color="green">
<button
class="self-start"
:disabled="!newDependencyProjectId"
@click="
() =>
addDependency(
toRaw({
project_id: newDependencyProjectId,
version_id: newDependencyVersionId || undefined,
dependency_type: newDependencyType,
}),
)
"
>
Add Dependency
</button>
</ButtonStyled>
</template>
<template v-if="newDependencyProjectId">
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Version </span>
<Combobox
v-model="newDependencyVersionId"
placeholder="Select version"
:options="newDependencyVersionOptions"
:search-value="selectedNewDependencyVersionLabel"
:searchable="true"
:select-search-text-on-focus="true"
/>
</div>
</div>
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-4">
<span class="font-semibold text-contrast">Suggested dependencies</span>
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
</div>
<div v-if="addedDependencies.length" class="flex flex-col gap-4">
<span class="font-semibold text-contrast">Added dependencies</span>
<DependenciesList />
</div>
<div class="grid gap-2.5">
<span class="font-semibold text-contrast"> Dependency relation </span>
<Combobox
v-model="newDependencyType"
placeholder="Select dependency type"
:options="[
{ label: 'Required', value: 'required' },
{ label: 'Optional', value: 'optional' },
{ label: 'Incompatible', value: 'incompatible' },
{ label: 'Embedded', value: 'embedded' },
]"
/>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import {
ButtonStyled,
Combobox,
injectModrinthClient,
injectNotificationManager,
} from '@modrinth/ui'
import type { ComboboxOption } from '@modrinth/ui/src/components/base/Combobox.vue'
import { Combobox } from '@modrinth/ui'
import { computed } from 'vue'
import DependencySelect from '~/components/ui/create-project-version/components/DependencySelect.vue'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
import DependenciesList from '../components/DependenciesList.vue'
import SuggestedDependencies from '../components/SuggestedDependencies/SuggestedDependencies.vue'
const { newDependencyProjectId, newDependencyType, newDependencyVersionId, newDependencyVersions } =
injectManageVersionContext()
const { addNotification } = injectNotificationManager()
const { labrinth } = injectModrinthClient()
const {
draftVersion,
dependencyProjects,
dependencyVersions,
projectsFetchLoading,
visibleSuggestedDependencies,
} = injectManageVersionContext()
const errorNotification = (err: any) => {
addNotification({
title: 'An error occurred',
text: err.data ? err.data.description : err,
type: 'error',
})
}
const newDependencyProjectId = ref<string>()
const newDependencyType = ref<Labrinth.Versions.v2.DependencyType>('required')
const newDependencyVersionId = ref<string | null>(null)
const newDependencyVersions = ref<ComboboxOption<string>[]>([])
// reset to defaults when select different project
watch(newDependencyProjectId, async () => {
newDependencyVersionId.value = null
newDependencyType.value = 'required'
if (!newDependencyProjectId.value) {
newDependencyVersions.value = []
} else {
try {
const versions = await labrinth.versions_v3.getProjectVersions(newDependencyProjectId.value)
newDependencyVersions.value = versions.map((version) => ({
label: version.name,
value: version.id,
}))
} catch (error: any) {
errorNotification(error)
}
}
})
const addedDependencies = computed(() =>
(draftVersion.value.dependencies || [])
.map((dep) => {
if (!dep.project_id) return null
const dependencyProject = dependencyProjects.value[dep.project_id]
const versionName = dependencyVersions.value[dep.version_id || '']?.name ?? ''
if (!dependencyProject && projectsFetchLoading.value) return null
return {
projectId: dep.project_id,
name: dependencyProject?.name,
icon: dependencyProject?.icon_url,
dependencyType: dep.dependency_type,
versionName,
}
})
.filter(Boolean),
const newDependencyVersionOptions = computed(() => [
{ label: 'Any version', value: null },
...newDependencyVersions.value,
])
const selectedNewDependencyVersionLabel = computed(
() =>
newDependencyVersionOptions.value.find(
(option) => option.value === newDependencyVersionId.value,
)?.label,
)
const addDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
if (!draftVersion.value.dependencies) draftVersion.value.dependencies = []
const alreadyAdded = draftVersion.value.dependencies.some((existing) => {
if (existing.project_id !== dependency.project_id) return false
if (!existing.version_id && !dependency.version_id) return true
return existing.version_id === dependency.version_id
})
if (alreadyAdded) {
addNotification({
title: 'Dependency already added',
text: 'You cannot add the same dependency twice.',
type: 'error',
})
return
}
projectsFetchLoading.value = true
draftVersion.value.dependencies.push(dependency)
newDependencyProjectId.value = undefined
}
const handleAddSuggestedDependency = (dependency: Labrinth.Versions.v3.Dependency) => {
draftVersion.value.dependencies?.push({
project_id: dependency.project_id,
version_id: dependency.version_id,
dependency_type: dependency.dependency_type,
})
}
</script>
@@ -1,11 +1,10 @@
<template>
<NavTabs
<Tabs
v-if="editingVersion"
mode="local"
:links="editTabLinks"
:active-index="1"
class="mb-4 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
value="add-details"
:tabs="editTabs"
class="mb-5 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
/>
<div class="flex w-full flex-col gap-6">
<div class="flex flex-col gap-2">
@@ -18,6 +17,7 @@
:never-empty="true"
:capitalize="true"
:disabled="isUploading"
hide-checkmark-icon
/>
</div>
<div class="flex flex-col gap-2">
@@ -46,7 +46,7 @@
/>
</div>
<div class="flex flex-col gap-2">
<span class="font-semibold text-contrast"> Version changlog </span>
<span class="font-semibold text-contrast"> Version changelog </span>
<div class="w-full">
<MarkdownEditor
@@ -61,25 +61,21 @@
</template>
<script lang="ts" setup>
import { Chips, MarkdownEditor, NavTabs, StyledInput } from '@modrinth/ui'
import { Chips, MarkdownEditor, StyledInput, Tabs, type TabsTab } from '@modrinth/ui'
import { useImageUpload } from '~/composables/image-upload.ts'
import { injectManageVersionContext } from '~/providers/version/manage-version-modal'
const { draftVersion, isUploading, editingVersion, modal } = injectManageVersionContext()
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
const editTabLinks = editTabs.map(({ label, href }) => ({ label, href }))
function setEditTab(index: number) {
const tab = editTabs[index]
if (!tab) return
modal.value?.setStage(tab.stage)
function setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
}
async function onImageUpload(file: File) {
@@ -1,11 +1,10 @@
<template>
<NavTabs
<Tabs
v-if="editingVersion"
mode="local"
:links="editTabLinks"
:active-index="0"
class="mb-2 border border-solid border-surface-5 shadow-none drop-shadow-none"
@tab-click="setEditTab"
value="metadata"
:tabs="editTabs"
class="mb-3 border border-solid border-surface-5 !shadow-none !drop-shadow-none"
@change="setEditTab"
/>
<div class="flex flex-col gap-6">
<div v-if="!editingVersion" class="flex flex-col gap-1">
@@ -161,40 +160,32 @@
</template>
<template v-if="!noDependenciesProject">
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast"> Suggested dependencies </span>
<div class="flex flex-col gap-2.5">
<div class="flex flex-col gap-1">
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast"> Dependencies </span>
<ButtonStyled type="transparent" size="standard">
<button @click="editDependencies">
<EditIcon />
Edit
</button>
</ButtonStyled>
</div>
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
</div>
<ButtonStyled type="transparent" size="standard">
<button @click="addDependency">
<PlusIcon />
Add dependency
</button>
</ButtonStyled>
</div>
<div
v-if="!visibleSuggestedDependencies.length || draftVersion.dependencies?.length"
class="flex flex-col gap-1"
>
<div class="flex items-center justify-between">
<span class="font-semibold text-contrast"> Dependencies </span>
<ButtonStyled type="transparent" size="standard">
<button @click="editDependencies">
<EditIcon />
Edit
</button>
</ButtonStyled>
<div v-if="draftVersion.dependencies?.length" class="flex flex-col gap-4">
<DependenciesList />
</div>
<div v-else class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
<span class="text-sm font-medium">No dependencies added.</span>
</div>
</div>
<div v-if="draftVersion.dependencies?.length" class="flex flex-col gap-4">
<DependenciesList disable-remove />
</div>
<div v-else class="flex flex-col gap-1.5 gap-y-4 rounded-xl bg-surface-2 p-3 py-4">
<span class="text-sm font-medium">No dependencies added.</span>
<div v-if="visibleSuggestedDependencies.length" class="flex flex-col gap-2.5">
<div class="flex items-center justify-between">
<span class="font-medium"> Suggested </span>
</div>
<SuggestedDependencies @on-add-suggestion="handleAddSuggestedDependency" />
</div>
</div>
</template>
@@ -203,14 +194,15 @@
<script lang="ts" setup>
import type { Labrinth } from '@modrinth/api-client'
import { EditIcon, getLoaderIcon, UnknownIcon } from '@modrinth/assets'
import { EditIcon, getLoaderIcon, PlusIcon, UnknownIcon } from '@modrinth/assets'
import {
ButtonStyled,
defineMessages,
ENVIRONMENTS_COPY,
FormattedTag,
injectProjectPageContext,
NavTabs,
Tabs,
type TabsTab,
TagItem,
useVIntl,
} from '@modrinth/ui'
@@ -239,18 +231,14 @@ const { projectV2 } = injectProjectPageContext()
const generatedState = useGeneratedState()
const loaders = computed(() => generatedState.value.loaders)
const editTabs = [
{ label: 'Metadata', href: 'metadata', stage: 'metadata' },
{ label: 'Details', href: 'details', stage: 'add-details' },
{ label: 'Files', href: 'files', stage: 'add-files' },
] as const
const editTabs: TabsTab[] = [
{ label: 'Metadata', value: 'metadata' },
{ label: 'Details', value: 'add-details' },
{ label: 'Files', value: 'add-files' },
]
const editTabLinks = editTabs.map(({ label, href }) => ({ label, href }))
function setEditTab(index: number) {
const tab = editTabs[index]
if (!tab) return
modal.value?.setStage(tab.stage)
function setEditTab(tab: TabsTab) {
modal.value?.setStage(tab.value)
}
const isModpack = computed(() => projectType.value === 'modpack')
@@ -279,7 +267,7 @@ const editEnvironment = () => {
const editFiles = () => {
modal.value?.setStage('from-details-files')
}
const editDependencies = () => {
const addDependency = () => {
modal.value?.setStage('from-details-dependencies')
}
@@ -131,10 +131,9 @@
class="h-10"
/>
<StyledInput
<DatePicker
v-else-if="field.type === 'date'"
v-model="formData[field.name]"
type="date"
wrapper-class="w-full"
/>
@@ -230,6 +229,7 @@ import {
Admonition,
Checkbox,
Combobox,
DatePicker,
defineMessages,
financialMessages,
formFieldLabels,
@@ -95,11 +95,11 @@
<span class="text-red">*</span>
</span>
</label>
<StyledInput
<DatePicker
v-model="formData.dateOfBirth"
type="date"
:max="maxDate"
:max-date="maxDate"
autocomplete="bday"
placeholder="Select date of birth"
wrapper-class="w-full"
/>
</div>
@@ -213,6 +213,7 @@
import {
Chips,
Combobox,
DatePicker,
defineMessages,
formFieldLabels,
formFieldPlaceholders,
@@ -233,7 +233,7 @@ const visibleNags = computed<Nag[]>(() => {
link: {
path: 'moderation',
title: messages.visitModerationPage,
shouldShow: () => props.routeName !== 'type-id-moderation',
shouldShow: () => props.routeName !== 'type-project-moderation',
},
})
}
@@ -1,5 +1,5 @@
<template>
<div class="shadow-card rounded-2xl border border-solid border-surface-5 bg-surface-3 p-4">
<div class="shadow-card rounded-2xl border border-solid border-surface-4 bg-surface-3 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<NuxtLink
@@ -48,6 +48,16 @@
<span class="text-sm text-secondary">Requesting</span>
<Badge :type="queueEntry.project.requested_status" class="text-sm" />
</div>
<div
v-if="showExternalDependencies"
v-tooltip="'External dependencies'"
class="flex items-center gap-1 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
>
<FileIcon aria-hidden="true" class="size-4 text-secondary" />
<span class="text-sm font-medium text-secondary">
{{ queueEntry.external_dependencies_count }}
</span>
</div>
</div>
<div v-if="queueEntry.ownership?.kind === 'user'">
<NuxtLink
@@ -107,8 +117,8 @@
<LinkIcon />
</button>
</ButtonStyled>
<ButtonStyled v-tooltip="'Begin review'" circular color="orange">
<button @click="openProjectForReview">
<ButtonStyled circular color="orange">
<button v-tooltip="'Begin review'" @click="openProjectForReview">
<ScaleIcon />
</button>
</ButtonStyled>
@@ -119,7 +129,7 @@
</template>
<script setup lang="ts">
import { ClipboardCopyIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import { ClipboardCopyIcon, FileIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
import {
Avatar,
Badge,
@@ -151,6 +161,7 @@ const formatDateTimeFull = useFormatDateTime({
const props = defineProps<{
queueEntry: ModerationProject
showExternalDependencies?: boolean
}>()
const emit = defineEmits<{

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