mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
169
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f9e9e0de7 | ||
|
|
ad969a346a | ||
|
|
877afa7278 | ||
|
|
fe6772874a | ||
|
|
93a1fc29f0 | ||
|
|
47cd7508cf | ||
|
|
63a60b292c | ||
|
|
913dde963b | ||
|
|
b50d1d07de | ||
|
|
d1ca6f4933 | ||
|
|
13588fc33d | ||
|
|
d8633df1b3 | ||
|
|
9bdb1ef5bd | ||
|
|
70cc6c3a62 | ||
|
|
e8ea8729f3 | ||
|
|
ed87b627cf | ||
|
|
dcfa5725ba | ||
|
|
157208768c | ||
|
|
de1bc58b01 | ||
|
|
c07727aa49 | ||
|
|
57a977f7f3 | ||
|
|
323d088ebd | ||
|
|
46f94bd067 | ||
|
|
adc1094cd5 | ||
|
|
6da553bc48 | ||
|
|
e877167db7 | ||
|
|
623b51e6ea | ||
|
|
5face9e56a | ||
|
|
40aff1e8bb | ||
|
|
5f8837604d | ||
|
|
795dad040f | ||
|
|
1067894a82 | ||
|
|
dbea158e16 | ||
|
|
b945871320 | ||
|
|
5511662a94 | ||
|
|
9ca181dc1a | ||
|
|
c84b658e41 | ||
|
|
a562bf9e8d | ||
|
|
a1876eb3ec | ||
|
|
17852f959b | ||
|
|
9006dce2b0 | ||
|
|
efdfb51149 | ||
|
|
79cc5fef72 | ||
|
|
20ed8e22cf | ||
|
|
ddefc28ca6 | ||
|
|
654757f9fb | ||
|
|
407641ee7c | ||
|
|
96ab120cd0 | ||
|
|
3ed75a146a | ||
|
|
5a1c2a46bc | ||
|
|
df8ebbd3e0 | ||
|
|
a34f5b18c9 | ||
|
|
92f8eb40d5 | ||
|
|
38f02ea9a0 | ||
|
|
44ce6bfc33 | ||
|
|
a1ea6903f5 | ||
|
|
d88d555329 | ||
|
|
0723ef0aa5 | ||
|
|
5b86bb8c4a | ||
|
|
517c3d2d72 | ||
|
|
7f15772f59 | ||
|
|
0905ea72f6 | ||
|
|
8d20fd82db | ||
|
|
c831e38e32 | ||
|
|
40b8fb3a4a | ||
|
|
977bb2ff58 | ||
|
|
732bdfc79b | ||
|
|
1cce1eb248 | ||
|
|
76320f227f | ||
|
|
34228a5b6c | ||
|
|
6d66aee4ec | ||
|
|
fc7be043c7 | ||
|
|
7fa88e5d4d | ||
|
|
21508e0637 | ||
|
|
4f5f3bf1b1 | ||
|
|
ad4912d64d | ||
|
|
0002ae2479 | ||
|
|
e4568330a7 | ||
|
|
b0108e0624 | ||
|
|
ac63abadb7 | ||
|
|
fa749b5aff | ||
|
|
a2adbe18c4 | ||
|
|
0fcbe51816 | ||
|
|
23cfeca91d | ||
|
|
db830ef65c | ||
|
|
08f44b3582 | ||
|
|
8ffc4e2599 | ||
|
|
58a5619073 | ||
|
|
4a6fa9fc3d | ||
|
|
b26d048a63 | ||
|
|
d5be2bd739 | ||
|
|
2dbc982175 | ||
|
|
50e776ba4b | ||
|
|
0dac5684af | ||
|
|
ca0b8d5ca4 | ||
|
|
b6651efe52 | ||
|
|
1286480a8a | ||
|
|
eb28121c0a | ||
|
|
a5ebc85356 | ||
|
|
70f08275af | ||
|
|
2da493e641 | ||
|
|
2351195d70 | ||
|
|
7726b2a5a3 | ||
|
|
fa286a7174 | ||
|
|
d6bd6a2c60 | ||
|
|
c530671be1 | ||
|
|
bb6d8a6fc4 | ||
|
|
256e8f946a | ||
|
|
60fa3b7609 | ||
|
|
7e5590b8b9 | ||
|
|
d20f05fb04 | ||
|
|
a00a7971d1 | ||
|
|
26e98b7155 | ||
|
|
77b06f679e | ||
|
|
50e9472dd9 | ||
|
|
52c38f7506 | ||
|
|
4296e3b51f | ||
|
|
23ce9bc129 | ||
|
|
b569354793 | ||
|
|
e9c141298d | ||
|
|
7c7f3584ac | ||
|
|
0ec70d29b9 | ||
|
|
b667b3ae9e | ||
|
|
16de6182e4 | ||
|
|
364fed52dd | ||
|
|
0fe695721a | ||
|
|
692f22b749 | ||
|
|
ed7792097d | ||
|
|
c56ce6c9ee | ||
|
|
91bc58b8ee | ||
|
|
38d757b66a | ||
|
|
862b54f00d | ||
|
|
c2f4835fe7 | ||
|
|
b97c191d75 | ||
|
|
b0c6a06909 | ||
|
|
fc3e384f54 | ||
|
|
87ff8fd4d9 | ||
|
|
288c1fb970 | ||
|
|
f6e6ac28d4 | ||
|
|
4a972bca73 | ||
|
|
b09c26f616 | ||
|
|
204bd52a5a | ||
|
|
b59f327b04 | ||
|
|
89e643fa50 | ||
|
|
cee25fdbbc | ||
|
|
b95a2d875a | ||
|
|
1067b02830 | ||
|
|
ebf4a6ef5f | ||
|
|
e4376c2b15 | ||
|
|
5fe07de187 | ||
|
|
80d66d3c6e | ||
|
|
64f7fb811e | ||
|
|
734720e11e | ||
|
|
ef4044534f | ||
|
|
6fc741f7c0 | ||
|
|
527757d158 | ||
|
|
f4a2999b20 | ||
|
|
586cdc90f9 | ||
|
|
e56f3ae18a | ||
|
|
c392c85917 | ||
|
|
29110890e1 | ||
|
|
a18f67bf0c | ||
|
|
623e01dbff | ||
|
|
14c728c8e3 | ||
|
|
a1b2073f57 | ||
|
|
45c417e406 | ||
|
|
47c60788ff | ||
|
|
3c60cc6f08 | ||
|
|
06353bbc14 |
@@ -81,6 +81,6 @@ Please follow these rules precisely:
|
||||
Use existing patterns from our codebase:
|
||||
|
||||
- Variables/plurals: see `apps/frontend/src/pages/frog.vue`
|
||||
- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`
|
||||
- Rich-text link tags: see `apps/frontend/src/error.vue`
|
||||
|
||||
When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or `<IntlFormatted>`.
|
||||
|
||||
@@ -76,13 +76,6 @@ jobs:
|
||||
env:
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
@@ -96,6 +89,9 @@ jobs:
|
||||
rustflags: ''
|
||||
cache: false
|
||||
|
||||
- name: Setup mold
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
|
||||
@@ -78,14 +78,8 @@ jobs:
|
||||
GIT_HASH: ${{ github.sha }}
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
RUST_MIN_STACK: '16777216'
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -100,10 +94,11 @@ jobs:
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Install build dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
with:
|
||||
packages: cmake libcurl4-openssl-dev
|
||||
version: v2 # cache key
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake libcurl4-openssl-dev
|
||||
#uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
#with:
|
||||
# packages: cmake libcurl4-openssl-dev
|
||||
# version: v2 # cache key
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
|
||||
@@ -84,6 +84,10 @@ jobs:
|
||||
rustflags: ''
|
||||
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Setup mold
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
|
||||
@@ -81,13 +81,6 @@ jobs:
|
||||
# blacksmith runner)
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
|
||||
steps:
|
||||
@@ -129,6 +122,9 @@ jobs:
|
||||
components: clippy, rustfmt
|
||||
cache: false
|
||||
|
||||
- name: Setup mold
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5
|
||||
with:
|
||||
|
||||
@@ -50,7 +50,7 @@ Run these from the **root** folder before opening a pull request - do not run th
|
||||
- **App frontend:** `pnpm prepr:frontend:app`
|
||||
- **Frontend libs:** `pnpm prepr:frontend:lib`
|
||||
- **All frontend (app+web):** `pnpm prepr`
|
||||
- **Labrinth (backend):** See `apps/labrinth/CLAUDE.md`
|
||||
- **Labrinth (backend):** See `apps/labrinth/AGENTS.md`
|
||||
|
||||
The website and app `prepr` commands
|
||||
|
||||
@@ -62,7 +62,7 @@ The website and app `prepr` commands
|
||||
|
||||
## Project-Specific Instructions
|
||||
|
||||
Each project may have its own `CLAUDE.md` with detailed instructions:
|
||||
Each project may have its own file with detailed instructions:
|
||||
|
||||
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
|
||||
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
|
||||
|
||||
Generated
+615
-108
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -9,6 +9,7 @@ members = [
|
||||
"packages/ariadne",
|
||||
"packages/daedalus",
|
||||
"packages/labrinth-derive",
|
||||
"packages/modrinth-content-management",
|
||||
"packages/modrinth-log",
|
||||
"packages/modrinth-maxmind",
|
||||
"packages/modrinth-util",
|
||||
@@ -32,7 +33,6 @@ actix-ws = "0.3.0"
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async-compression = { version = "0.4.32", default-features = false }
|
||||
async-minecraft-ping = { path = "packages/async-minecraft-ping" }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
@@ -51,6 +51,7 @@ aws-sdk-s3 = { version = "=1.122.0", default-features = false, features = [
|
||||
] }
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.4"
|
||||
bon = "3.9.3"
|
||||
bytemuck = "1.24.0"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
@@ -119,6 +120,7 @@ lettre = { version = "0.11.19", default-features = false, features = [
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||
modrinth-content-management = { path = "packages/modrinth-content-management" }
|
||||
modrinth-log = { path = "packages/modrinth-log" }
|
||||
modrinth-util = { path = "packages/modrinth-util" }
|
||||
muralpay = { path = "packages/muralpay" }
|
||||
@@ -157,6 +159,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
|
||||
] }
|
||||
rustls = "0.23.32"
|
||||
rusty-money = "0.4.1"
|
||||
scalar_api_reference = { version = "0.2.2", default-features = false }
|
||||
secrecy = "0.10.3"
|
||||
sentry = { version = "0.45.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
@@ -216,10 +219,10 @@ typed-path = "0.12.0"
|
||||
url = "2.5.7"
|
||||
urlencoding = "2.1.3"
|
||||
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
|
||||
utoipa-actix-web = { version = "0.1.2" }
|
||||
utoipa-scalar = { version = "0.3.0", default-features = false }
|
||||
uuid = "1.18.1"
|
||||
validator = "0.20.0"
|
||||
webauthn-rs = "0.5.5"
|
||||
webauthn-rs-proto = "0.5.5"
|
||||
webp = { version = "0.3.1", default-features = false }
|
||||
webview2-com = "0.38.0" # Should be updated in lockstep with wry
|
||||
whoami = "1.6.1"
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Modrinth Monorepo
|
||||
|
||||
Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains  lines of code and has  contributors!
|
||||
Welcome to the Modrinth Monorepo, the primary codebase for the Modrinth web interface and app. It contains  lines of code and has  contributors!
|
||||
|
||||
If you're not a developer and you've stumbled upon this repository, you can access the web interface on the [Modrinth website](https://modrinth.com) and download the latest release of the app [here](https://modrinth.com/app).
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
HomeIcon,
|
||||
LeftArrowIcon,
|
||||
LibraryIcon,
|
||||
LinkIcon,
|
||||
LogInIcon,
|
||||
LogOutIcon,
|
||||
NewspaperIcon,
|
||||
@@ -85,6 +86,7 @@ import NavButton from '@/components/ui/NavButton.vue'
|
||||
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
|
||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import WindowControls from '@/components/ui/WindowControls.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
@@ -94,9 +96,9 @@ import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user, get_version } from '@/helpers/cache.js'
|
||||
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
|
||||
import { list, run } from '@/helpers/instance'
|
||||
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'
|
||||
@@ -110,6 +112,7 @@ import {
|
||||
isNetworkMetered,
|
||||
setRestartAfterPendingUpdate,
|
||||
} from '@/helpers/utils.js'
|
||||
import { start_join_server, start_join_singleplayer_world } from '@/helpers/worlds.ts'
|
||||
import i18n from '@/i18n.config'
|
||||
import {
|
||||
appUpdateState,
|
||||
@@ -146,6 +149,7 @@ const APP_SIDEBAR_WIDTH = 300
|
||||
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
||||
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
|
||||
const credentials = ref()
|
||||
let credentialsRefreshId = 0
|
||||
const sidebarToggled = ref(true)
|
||||
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
|
||||
sidebarToggled.value = !themeStore.toggleSidebar
|
||||
@@ -636,6 +640,7 @@ const contentInstallModpackAlreadyInstalledModal = ref()
|
||||
const addServerToInstanceModal = ref()
|
||||
const incompatibilityWarningModal = ref()
|
||||
const installToPlayModal = ref()
|
||||
const sharedInstanceInviteHandler = ref()
|
||||
const updateToPlayModal = ref()
|
||||
|
||||
const modrinthLoginFlowWaitModal = ref()
|
||||
@@ -646,8 +651,8 @@ watch(incompatibilityWarningModal, (modal) => {
|
||||
}
|
||||
})
|
||||
|
||||
setupAuthProvider(credentials, async (_redirectPath) => {
|
||||
await signIn()
|
||||
setupAuthProvider(credentials, async (_redirectPath, flow) => {
|
||||
await signIn(flow)
|
||||
})
|
||||
|
||||
async function validateSession(sessionToken) {
|
||||
@@ -664,23 +669,33 @@ async function validateSession(sessionToken) {
|
||||
}
|
||||
|
||||
async function fetchCredentials() {
|
||||
const refreshId = ++credentialsRefreshId
|
||||
credentials.value = undefined
|
||||
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
|
||||
if (creds && creds.user_id) {
|
||||
if (creds.session && !(await validateSession(creds.session))) {
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
|
||||
await logout().catch(handleError)
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
|
||||
credentials.value = null
|
||||
return
|
||||
}
|
||||
creds.user = await get_user(creds.user_id, 'bypass').catch(handleError)
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
}
|
||||
credentials.value = creds ?? null
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
async function signIn(flow = 'sign-in') {
|
||||
modrinthLoginFlowWaitModal.value.show()
|
||||
|
||||
try {
|
||||
await login()
|
||||
await login(flow)
|
||||
await fetchCredentials()
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -698,6 +713,13 @@ async function signIn() {
|
||||
}
|
||||
|
||||
async function logOut() {
|
||||
await performLogOut()
|
||||
}
|
||||
|
||||
async function performLogOut() {
|
||||
credentialsRefreshId++
|
||||
credentials.value = undefined
|
||||
|
||||
await logout().catch(handleError)
|
||||
await fetchCredentials()
|
||||
}
|
||||
@@ -820,30 +842,34 @@ function openServerInviteInviterProfile(inviterName) {
|
||||
}
|
||||
|
||||
async function handleLiveNotification(notification) {
|
||||
if (notification?.body?.type !== 'server_invite' || notification.read) return
|
||||
if (displayedServerInviteNotifications.has(notification.id)) return
|
||||
if (!notification?.body || notification.read) return
|
||||
if (await sharedInstanceInviteHandler.value?.handleNotification(notification)) return
|
||||
|
||||
displayedServerInviteNotifications.add(notification.id)
|
||||
if (notification.body.type === 'server_invite') {
|
||||
if (displayedServerInviteNotifications.has(notification.id)) return
|
||||
|
||||
const serverName =
|
||||
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
|
||||
const inviterId = notification.body.invited_by
|
||||
const invitedBy =
|
||||
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
|
||||
displayedServerInviteNotifications.add(notification.id)
|
||||
|
||||
addPopupNotification({
|
||||
title: serverName,
|
||||
autoCloseMs: null,
|
||||
toast: {
|
||||
type: 'server-invite',
|
||||
actorName: invitedBy?.username ?? null,
|
||||
actorAvatarUrl: invitedBy?.avatar_url ?? null,
|
||||
entityName: serverName,
|
||||
onAccept: () => acceptServerInviteNotification(notification),
|
||||
onDecline: () => declineServerInviteNotification(notification),
|
||||
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
|
||||
},
|
||||
})
|
||||
const serverName =
|
||||
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
|
||||
const inviterId = notification.body.invited_by
|
||||
const invitedBy =
|
||||
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
|
||||
|
||||
addPopupNotification({
|
||||
title: serverName,
|
||||
autoCloseMs: null,
|
||||
toast: {
|
||||
type: 'server-invite',
|
||||
actorName: invitedBy?.username ?? null,
|
||||
actorAvatarUrl: invitedBy?.avatar_url ?? null,
|
||||
entityName: serverName,
|
||||
onAccept: () => acceptServerInviteNotification(notification),
|
||||
onDecline: () => declineServerInviteNotification(notification),
|
||||
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCommand(e) {
|
||||
@@ -852,13 +878,32 @@ async function handleCommand(e) {
|
||||
if (e.event === 'RunMRPack') {
|
||||
// RunMRPack should directly install a local mrpack given a path
|
||||
if (e.path.endsWith('.mrpack')) {
|
||||
await create_profile_and_install_from_file(e.path, (createProfile, fileName) =>
|
||||
unknownPackWarningModal.value?.show(createProfile, fileName),
|
||||
).catch(handleError)
|
||||
const location = { type: 'fromFile', path: e.path }
|
||||
const preview = await install_get_modpack_preview(location).catch(handleError)
|
||||
if (preview?.unknownFile) {
|
||||
const splitPath = e.path.split(/[\\/]/)
|
||||
const fileName = splitPath ? splitPath[splitPath.length - 1] : e.path
|
||||
unknownPackWarningModal.value?.show(
|
||||
() => install_create_modpack_instance(location).then(() => undefined),
|
||||
fileName,
|
||||
)
|
||||
} else {
|
||||
await install_create_modpack_instance(location).catch(handleError)
|
||||
}
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
}
|
||||
} else if (e.event === 'LaunchInstance') {
|
||||
if (e.server) {
|
||||
await start_join_server(e.id, e.server).catch(handleError)
|
||||
} else if (e.singleplayer_world) {
|
||||
await start_join_singleplayer_world(e.id, e.singleplayer_world).catch(handleError)
|
||||
} else {
|
||||
await run(e.id).catch(handleError)
|
||||
}
|
||||
} else if (e.event === 'InstallSharedInstanceInvite') {
|
||||
await sharedInstanceInviteHandler.value?.installFromInviteId(e.invite_id)
|
||||
} else if (e.event === 'InstallServer') {
|
||||
await router.push(`/project/${e.id}`)
|
||||
await playServerProject(e.id).catch(handleError)
|
||||
@@ -1323,12 +1368,11 @@ async function processPendingSurveys() {
|
||||
const creds = await getCreds().catch(handleError)
|
||||
const userId = creds?.user_id
|
||||
|
||||
const instances = await list().catch(handleError)
|
||||
const isActivePlayer =
|
||||
instances.findIndex(
|
||||
(instance) =>
|
||||
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
|
||||
) >= 0
|
||||
const instances = (await list().catch(handleError)) ?? []
|
||||
const isActivePlayer = instances.some(
|
||||
(instance) =>
|
||||
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
|
||||
)
|
||||
|
||||
let surveys = []
|
||||
try {
|
||||
@@ -1450,6 +1494,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
>
|
||||
<PlusIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Install from invite link'"
|
||||
:to="(event) => sharedInstanceInviteHandler?.showManualInviteLinkModal(event)"
|
||||
>
|
||||
<LinkIcon />
|
||||
</NavButton>
|
||||
<div class="flex flex-grow"></div>
|
||||
<NavButton
|
||||
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
|
||||
@@ -1719,6 +1769,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
|
||||
@go-to-instance="handleContentInstallModpackDuplicateGoToInstance"
|
||||
/>
|
||||
<SharedInstanceInviteHandler ref="sharedInstanceInviteHandler" />
|
||||
<InstallToPlayModal ref="installToPlayModal" />
|
||||
<UpdateToPlayModal ref="updateToPlayModal" />
|
||||
</template>
|
||||
|
||||
@@ -24,7 +24,8 @@ import { computed, ref } from 'vue'
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import Instance from '@/components/ui/Instance.vue'
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import { duplicate, remove } from '@/helpers/profile.js'
|
||||
import { install_duplicate_instance } from '@/helpers/install'
|
||||
import { remove } from '@/helpers/instance'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
@@ -48,21 +49,21 @@ const instanceComponents = ref(null)
|
||||
const currentDeleteInstance = ref(null)
|
||||
const confirmModal = ref(null)
|
||||
|
||||
async function deleteProfile() {
|
||||
async function deleteInstance() {
|
||||
if (currentDeleteInstance.value) {
|
||||
instanceComponents.value = instanceComponents.value.filter(
|
||||
(x) => x.instance.path !== currentDeleteInstance.value,
|
||||
(x) => x.instance.id !== currentDeleteInstance.value,
|
||||
)
|
||||
await remove(currentDeleteInstance.value).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateProfile(p) {
|
||||
await duplicate(p).catch(handleError)
|
||||
async function duplicateInstance(p) {
|
||||
await install_duplicate_instance(p).catch(handleError)
|
||||
}
|
||||
|
||||
const handleRightClick = (event, profilePathId) => {
|
||||
const item = instanceComponents.value.find((x) => x.instance.path === profilePathId)
|
||||
const handleRightClick = (event, instanceId) => {
|
||||
const item = instanceComponents.value.find((x) => x.instance.id === instanceId)
|
||||
const baseOptions = [
|
||||
{ name: 'add_content' },
|
||||
{ type: 'divider' },
|
||||
@@ -114,16 +115,16 @@ const handleOptionsClick = async (args) => {
|
||||
break
|
||||
case 'duplicate':
|
||||
if (args.item.instance.install_stage == 'installed')
|
||||
await duplicateProfile(args.item.instance.path)
|
||||
await duplicateInstance(args.item.instance.id)
|
||||
break
|
||||
case 'open':
|
||||
await args.item.openFolder()
|
||||
break
|
||||
case 'copy':
|
||||
await navigator.clipboard.writeText(args.item.instance.path)
|
||||
await navigator.clipboard.writeText(args.item.instance.id)
|
||||
break
|
||||
case 'delete':
|
||||
currentDeleteInstance.value = args.item.instance.path
|
||||
currentDeleteInstance.value = args.item.instance.id
|
||||
confirmModal.value.show()
|
||||
break
|
||||
}
|
||||
@@ -321,13 +322,13 @@ const filteredResults = computed(() => {
|
||||
<Instance
|
||||
v-for="instance in instanceSection.value"
|
||||
ref="instanceComponents"
|
||||
:key="instance.path + instance.install_stage"
|
||||
:key="instance.id + instance.install_stage"
|
||||
:instance="instance"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.id)"
|
||||
/>
|
||||
</section>
|
||||
</Accordion>
|
||||
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteProfile" />
|
||||
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteInstance" />
|
||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
<template #stop> <StopCircleIcon /> Stop </template>
|
||||
|
||||
@@ -21,9 +21,10 @@ import Instance from '@/components/ui/Instance.vue'
|
||||
import LegacyProjectCard from '@/components/ui/LegacyProjectCard.vue'
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_by_profile_path } from '@/helpers/process.js'
|
||||
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { install_duplicate_instance } from '@/helpers/install'
|
||||
import { kill, remove, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process.js'
|
||||
import { showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
@@ -60,14 +61,14 @@ const deleteConfirmModal = ref(null)
|
||||
|
||||
const currentDeleteInstance = ref(null)
|
||||
|
||||
async function deleteProfile() {
|
||||
async function deleteInstance() {
|
||||
if (currentDeleteInstance.value) {
|
||||
await remove(currentDeleteInstance.value).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateProfile(p) {
|
||||
await duplicate(p).catch(handleError)
|
||||
async function duplicateInstance(p) {
|
||||
await install_duplicate_instance(p).catch(handleError)
|
||||
}
|
||||
|
||||
const handleInstanceRightClick = async (event, passedInstance) => {
|
||||
@@ -85,7 +86,7 @@ const handleInstanceRightClick = async (event, passedInstance) => {
|
||||
},
|
||||
]
|
||||
|
||||
const runningProcesses = await get_by_profile_path(passedInstance.path).catch(handleError)
|
||||
const runningProcesses = await get_by_instance_id(passedInstance.id).catch(handleError)
|
||||
|
||||
const options =
|
||||
runningProcesses.length > 0
|
||||
@@ -126,16 +127,14 @@ const handleProjectClick = (event, passedInstance) => {
|
||||
const handleOptionsClick = async (args) => {
|
||||
switch (args.option) {
|
||||
case 'play':
|
||||
await run(args.item.path).catch((err) =>
|
||||
handleSevereError(err, { profilePath: args.item.path }),
|
||||
)
|
||||
await run(args.item.id).catch((err) => handleSevereError(err, { instanceId: args.item.id }))
|
||||
trackEvent('InstanceStart', {
|
||||
loader: args.item.loader,
|
||||
game_version: args.item.game_version,
|
||||
})
|
||||
break
|
||||
case 'stop':
|
||||
await kill(args.item.path).catch(handleError)
|
||||
await kill(args.item.id).catch(handleError)
|
||||
trackEvent('InstanceStop', {
|
||||
loader: args.item.loader,
|
||||
game_version: args.item.game_version,
|
||||
@@ -144,26 +143,26 @@ const handleOptionsClick = async (args) => {
|
||||
case 'add_content':
|
||||
await router.push({
|
||||
path: `/browse/${args.item.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||
query: { i: args.item.path },
|
||||
query: { i: args.item.id },
|
||||
})
|
||||
break
|
||||
case 'edit':
|
||||
await router.push({
|
||||
path: `/instance/${encodeURIComponent(args.item.path)}`,
|
||||
path: `/instance/${encodeURIComponent(args.item.id)}`,
|
||||
})
|
||||
break
|
||||
case 'duplicate':
|
||||
if (args.item.install_stage == 'installed') await duplicateProfile(args.item.path)
|
||||
if (args.item.install_stage == 'installed') await duplicateInstance(args.item.id)
|
||||
break
|
||||
case 'delete':
|
||||
currentDeleteInstance.value = args.item.path
|
||||
currentDeleteInstance.value = args.item.id
|
||||
deleteConfirmModal.value.show()
|
||||
break
|
||||
case 'open_folder':
|
||||
await showProfileInFolder(args.item.path)
|
||||
await showInstanceInFolder(args.item.id)
|
||||
break
|
||||
case 'copy_path':
|
||||
await navigator.clipboard.writeText(args.item.path)
|
||||
await navigator.clipboard.writeText(args.item.id)
|
||||
break
|
||||
case 'install': {
|
||||
await installVersion(
|
||||
@@ -239,7 +238,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="deleteProfile" />
|
||||
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="deleteInstance" />
|
||||
<div ref="rowContainer" class="flex flex-col gap-4">
|
||||
<div v-for="row in actualInstances" ref="rows" :key="row.label" class="row">
|
||||
<HeadingLink class="mt-1" :to="row.route">
|
||||
@@ -256,7 +255,7 @@ onUnmounted(() => {
|
||||
0,
|
||||
row.compact ? maxInstancesPerCompactRow : maxInstancesPerRow,
|
||||
)"
|
||||
:key="row.label + instance.path"
|
||||
:key="row.label + instance.id"
|
||||
:instance="instance"
|
||||
:compact="row.compact"
|
||||
:first="instanceIndex === 0"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { add_project_from_path } from '@/helpers/profile.js'
|
||||
import { add_project_from_path } from '@/helpers/instance'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
@@ -22,14 +22,14 @@ const handleAddContentFromFile = async () => {
|
||||
if (!newProject) return
|
||||
|
||||
for (const project of newProject) {
|
||||
await add_project_from_path(props.instance.path, project.path ?? project).catch(handleError)
|
||||
await add_project_from_path(props.instance.id, project.path ?? project).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchContent = async () => {
|
||||
await router.push({
|
||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'resourcepack' : 'mod'}`,
|
||||
query: { i: props.instance.path },
|
||||
query: { i: props.instance.id },
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,31 +14,7 @@
|
||||
<UnplugIcon class="text-secondary" />
|
||||
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
|
||||
</div>
|
||||
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
|
||||
<button
|
||||
v-if="showUpdatePill"
|
||||
type="button"
|
||||
class="!h-[34px] overflow-hidden text-sm !transition-[width,opacity,transform,background-color,color,filter] !duration-200 ease-out"
|
||||
:class="[
|
||||
updatePillWidthClass,
|
||||
{
|
||||
'update-pill-ready-hidden': finishedDownloading && !animateReadyPill,
|
||||
'update-pill-ready-visible': finishedDownloading && animateReadyPill,
|
||||
},
|
||||
]"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||
</span>
|
||||
<span v-else>{{ updateLabel }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<AppUpdateButton />
|
||||
<div
|
||||
class="flex border-solid border-surface-5 text-sm items-center gap-2 py-1.5 px-3 rounded-xl border"
|
||||
>
|
||||
@@ -47,28 +23,28 @@
|
||||
<div class="text-contrast flex items-center gap-2">
|
||||
<router-link
|
||||
v-tooltip="formatMessage(messages.viewInstance)"
|
||||
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
||||
:to="`/instance/${encodeURIComponent(selectedProcess.instance.id)}`"
|
||||
class="hover:underline"
|
||||
>
|
||||
{{ selectedProcess.profile.name }}
|
||||
{{ selectedProcess.instance.name }}
|
||||
</router-link>
|
||||
<Dropdown
|
||||
v-if="currentProcesses.length > 1"
|
||||
placement="bottom"
|
||||
:triggers="['click']"
|
||||
:hide-triggers="['click']"
|
||||
@show="showProfiles = true"
|
||||
@hide="showProfiles = false"
|
||||
@show="showInstances = true"
|
||||
@hide="showInstances = false"
|
||||
>
|
||||
<ButtonStyled type="transparent" circular size="small">
|
||||
<button
|
||||
v-tooltip="
|
||||
showProfiles
|
||||
showInstances
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
>
|
||||
<DropdownIcon :class="{ 'rotate-180': !!showProfiles }" />
|
||||
<DropdownIcon :class="{ 'rotate-180': !!showInstances }" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template #popper>
|
||||
@@ -93,7 +69,7 @@
|
||||
>
|
||||
<OnlineIndicatorIcon />
|
||||
<span class="mr-auto text-contrast flex items-center gap-2">
|
||||
{{ process.profile.name }}
|
||||
{{ process.instance.name }}
|
||||
<StarIcon v-if="process.uuid === selectedProcess.uuid" class="text-orange" />
|
||||
</span>
|
||||
</button>
|
||||
@@ -107,7 +83,7 @@
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.viewLogs)"
|
||||
class="active:scale-95 flex"
|
||||
@click.stop="goToTerminal(process.profile.path)"
|
||||
@click.stop="goToTerminal(process.instance.id)"
|
||||
>
|
||||
<TerminalSquareIcon class="text-secondary size-5" />
|
||||
</button>
|
||||
@@ -144,7 +120,6 @@ import {
|
||||
DownloadIcon,
|
||||
DropdownIcon,
|
||||
OnlineIndicatorIcon,
|
||||
RefreshCwIcon,
|
||||
StarIcon,
|
||||
StopCircleIcon,
|
||||
TerminalSquareIcon,
|
||||
@@ -161,21 +136,18 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { Dropdown } from 'floating-vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import AppUpdateButton from '@/components/ui/app-update-button/index.vue'
|
||||
import { useInstallJobNotifications } from '@/composables/browse/install-job-notifications'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { loading_listener, process_listener } from '@/helpers/events'
|
||||
import { get_many as getInstances } from '@/helpers/instance'
|
||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||
import { get_many as getInstances } from '@/helpers/profile.js'
|
||||
import type { LoadingBar } from '@/helpers/state'
|
||||
import { progress_bars_list } from '@/helpers/state'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import {
|
||||
appUpdateState,
|
||||
downloadAvailableAppUpdate,
|
||||
installAvailableAppUpdate,
|
||||
} from '@/providers/app-update'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
@@ -183,12 +155,12 @@ const { formatMessage } = useVIntl()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const showProfiles = ref(false)
|
||||
const showInstances = ref(false)
|
||||
|
||||
interface RunningProcess {
|
||||
uuid: string
|
||||
profile_path: string
|
||||
profile: GameInstance
|
||||
instance_id: string
|
||||
instance: GameInstance
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -240,96 +212,8 @@ const messages = defineMessages({
|
||||
id: 'app.action-bar.view-active-downloads',
|
||||
defaultMessage: 'View active downloads',
|
||||
},
|
||||
update: {
|
||||
id: 'app.action-bar.update',
|
||||
defaultMessage: 'Update',
|
||||
},
|
||||
downloadingUpdate: {
|
||||
id: 'app.action-bar.downloading-update',
|
||||
defaultMessage: 'Downloading update',
|
||||
},
|
||||
reloadToUpdate: {
|
||||
id: 'app.action-bar.reload-to-update',
|
||||
defaultMessage: 'Reload to update',
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
downloading,
|
||||
downloadPercent,
|
||||
downloadProgress,
|
||||
finishedDownloading,
|
||||
isVisible: isUpdateVisible,
|
||||
metered,
|
||||
restarting,
|
||||
} = appUpdateState
|
||||
|
||||
const isUpdateDownloading = computed(
|
||||
() =>
|
||||
downloading.value ||
|
||||
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
|
||||
)
|
||||
const showUpdatePill = computed(
|
||||
() => isUpdateVisible.value && (finishedDownloading.value || metered.value),
|
||||
)
|
||||
const animateReadyPill = ref(false)
|
||||
const updateLabel = computed(() => {
|
||||
if (isUpdateDownloading.value) {
|
||||
return formatMessage(messages.downloadingUpdate)
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
return formatMessage(messages.reloadToUpdate)
|
||||
}
|
||||
|
||||
return formatMessage(messages.update)
|
||||
})
|
||||
const updatePillWidthClass = computed(() => {
|
||||
if (isUpdateDownloading.value) {
|
||||
return 'w-[219px]'
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
return 'w-[166px]'
|
||||
}
|
||||
|
||||
return '!w-[96px]'
|
||||
})
|
||||
let readyPillAnimationFrame: number | null = null
|
||||
watch([showUpdatePill, finishedDownloading], async ([show, ready], [wasShown, wasReady]) => {
|
||||
if (readyPillAnimationFrame !== null) {
|
||||
cancelAnimationFrame(readyPillAnimationFrame)
|
||||
readyPillAnimationFrame = null
|
||||
}
|
||||
|
||||
if (!show || !ready) {
|
||||
animateReadyPill.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (wasShown && wasReady) {
|
||||
return
|
||||
}
|
||||
|
||||
animateReadyPill.value = false
|
||||
await nextTick()
|
||||
readyPillAnimationFrame = requestAnimationFrame(() => {
|
||||
animateReadyPill.value = true
|
||||
readyPillAnimationFrame = null
|
||||
})
|
||||
})
|
||||
async function handleUpdateClick() {
|
||||
if (isUpdateDownloading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
await installAvailableAppUpdate()
|
||||
} else {
|
||||
await downloadAvailableAppUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
const currentProcesses = ref<RunningProcess[]>([])
|
||||
const selectedProcess = ref<RunningProcess | undefined>()
|
||||
|
||||
@@ -337,22 +221,22 @@ const refresh = async () => {
|
||||
const processes = ((await getRunningProcesses().catch((error) => {
|
||||
handleError(error)
|
||||
return []
|
||||
})) ?? []) as Array<{ uuid: string; profile_path: string }>
|
||||
const paths = processes.map((process) => process.profile_path)
|
||||
const profiles: GameInstance[] = await getInstances(paths).catch((error) => {
|
||||
})) ?? []) as Array<{ uuid: string; instance_id: string }>
|
||||
const instanceIds = processes.map((process) => process.instance_id)
|
||||
const instances: GameInstance[] = await getInstances(instanceIds).catch((error) => {
|
||||
handleError(error)
|
||||
return []
|
||||
})
|
||||
|
||||
currentProcesses.value = processes
|
||||
.map((process) => {
|
||||
const profile = profiles.find((item) => process.profile_path === item.path)
|
||||
if (!profile) {
|
||||
const instance = instances.find((item) => process.instance_id === item.id)
|
||||
if (!instance) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...process,
|
||||
profile,
|
||||
instance,
|
||||
}
|
||||
})
|
||||
.filter((process): process is RunningProcess => process !== null)
|
||||
@@ -385,8 +269,8 @@ const stop = async (process: RunningProcess) => {
|
||||
await killProcess(process.uuid).catch(handleError)
|
||||
|
||||
trackEvent('InstanceStop', {
|
||||
loader: process.profile.loader,
|
||||
game_version: process.profile.game_version,
|
||||
loader: process.instance.loader,
|
||||
game_version: process.instance.game_version,
|
||||
source: 'AppBar',
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -395,12 +279,12 @@ const stop = async (process: RunningProcess) => {
|
||||
await refresh()
|
||||
}
|
||||
|
||||
function goToTerminal(path?: string) {
|
||||
const selectedPath = path ?? selectedProcess.value?.profile.path
|
||||
if (!selectedPath) {
|
||||
function goToTerminal(instanceId?: string) {
|
||||
const selectedInstanceId = instanceId ?? selectedProcess.value?.instance.id
|
||||
if (!selectedInstanceId) {
|
||||
return
|
||||
}
|
||||
router.push(`/instance/${encodeURIComponent(selectedPath)}/logs`)
|
||||
router.push(`/instance/${encodeURIComponent(selectedInstanceId)}/logs`)
|
||||
}
|
||||
|
||||
const currentLoadingBars = ref<LoadingBar[]>([])
|
||||
@@ -420,8 +304,7 @@ function getLoadingProgress(loadingBar: LoadingBar): number {
|
||||
}
|
||||
|
||||
function getLoadingText(loadingBar: LoadingBar): string {
|
||||
const percent = Math.floor(getLoadingProgress(loadingBar) * 100)
|
||||
return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%`
|
||||
return loadingBar.message ?? ''
|
||||
}
|
||||
|
||||
function getDisplayIconUrl(icon: string | null | undefined): string | null {
|
||||
@@ -453,25 +336,33 @@ function removeNotification(): void {
|
||||
}
|
||||
|
||||
function buildDownloadItems(): PopupNotificationProgressItem[] {
|
||||
return currentLoadingBars.value.map((bar) => ({
|
||||
id: getLoadingBarKey(bar),
|
||||
title: bar.title ?? '',
|
||||
text: getLoadingText(bar),
|
||||
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
|
||||
progress: getLoadingProgress(bar),
|
||||
waiting: !bar.total || bar.total <= 0,
|
||||
}))
|
||||
return [
|
||||
...installJobNotifications.progressItems.value,
|
||||
...currentLoadingBars.value.map((bar) => ({
|
||||
id: getLoadingBarKey(bar),
|
||||
title: bar.title ?? '',
|
||||
text: getLoadingText(bar),
|
||||
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
|
||||
progress: getLoadingProgress(bar),
|
||||
waiting: !bar.total || bar.total <= 0,
|
||||
progressType: 'percentage',
|
||||
progressCurrent: bar.current,
|
||||
progressTotal: bar.total,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
const hasVisibleActiveDownloadToasts = computed(() => !!getNotification())
|
||||
const hasActiveLoadingBars = computed(() => currentLoadingBars.value.length > 0)
|
||||
const hasActiveLoadingBars = computed(
|
||||
() => currentLoadingBars.value.length > 0 || installJobNotifications.active.value,
|
||||
)
|
||||
|
||||
function updateNotification(resummon = false): void {
|
||||
if (resummon) {
|
||||
dismissed.value = false
|
||||
}
|
||||
|
||||
if (currentLoadingBars.value.length === 0) {
|
||||
if (currentLoadingBars.value.length === 0 && !installJobNotifications.active.value) {
|
||||
removeNotification()
|
||||
dismissed.value = false
|
||||
return
|
||||
@@ -490,17 +381,23 @@ function updateNotification(resummon = false): void {
|
||||
const progressItems = buildDownloadItems()
|
||||
|
||||
if (notif) {
|
||||
notif.title = formatMessage(messages.downloads)
|
||||
notif.title = installJobNotifications.active.value
|
||||
? installJobNotifications.title.value
|
||||
: formatMessage(messages.downloads)
|
||||
notif.text = undefined
|
||||
notif.progressItems = progressItems
|
||||
notif.buttons = installJobNotifications.buttons.value
|
||||
notif.progress = undefined
|
||||
notif.waiting = undefined
|
||||
} else {
|
||||
notif = popupNotificationManager.addPopupNotification({
|
||||
title: formatMessage(messages.downloads),
|
||||
title: installJobNotifications.active.value
|
||||
? installJobNotifications.title.value
|
||||
: formatMessage(messages.downloads),
|
||||
type: 'download',
|
||||
autoCloseMs: null,
|
||||
progressItems,
|
||||
buttons: installJobNotifications.buttons.value,
|
||||
})
|
||||
notificationId.value = notif.id
|
||||
}
|
||||
@@ -513,8 +410,8 @@ function formatLoadingBars(loadingBar: LoadingBar): LoadingBar {
|
||||
version: formatted.bar_type.version,
|
||||
})
|
||||
}
|
||||
if (formatted.bar_type?.profile_path) {
|
||||
formatted.title = formatted.bar_type.profile_path
|
||||
if (formatted.bar_type?.instance_id) {
|
||||
formatted.title = formatted.bar_type.instance_name ?? formatted.bar_type.instance_id
|
||||
}
|
||||
if (formatted.bar_type?.pack_name) {
|
||||
formatted.title = formatted.bar_type.pack_name
|
||||
@@ -530,31 +427,41 @@ async function refreshLoadingBars() {
|
||||
|
||||
currentLoadingBars.value = Object.values(bars)
|
||||
.map(formatLoadingBars)
|
||||
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
|
||||
.filter(
|
||||
(bar) =>
|
||||
bar?.bar_type?.type !== 'launcher_update' &&
|
||||
![
|
||||
'java_download',
|
||||
'pack_file_download',
|
||||
'pack_download',
|
||||
'minecraft_download',
|
||||
'copy_instance',
|
||||
].includes(bar?.bar_type?.type ?? ''),
|
||||
)
|
||||
|
||||
const profilePaths = Array.from(
|
||||
const instanceIds = Array.from(
|
||||
new Set(
|
||||
currentLoadingBars.value
|
||||
.map((bar) => bar.bar_type?.profile_path)
|
||||
.filter((path): path is string => !!path),
|
||||
.map((bar) => bar.bar_type?.instance_id)
|
||||
.filter((instanceId): instanceId is string => !!instanceId),
|
||||
),
|
||||
)
|
||||
const profiles = profilePaths.length
|
||||
? await getInstances(profilePaths).catch((error) => {
|
||||
const instances = instanceIds.length
|
||||
? await getInstances(instanceIds).catch((error) => {
|
||||
handleError(error)
|
||||
return []
|
||||
})
|
||||
: []
|
||||
const profileIconUrls = new Map(
|
||||
profiles.map((profile) => [profile.path, getDisplayIconUrl(profile.icon_path)]),
|
||||
const instanceIconUrls = new Map(
|
||||
instances.map((instance) => [instance.id, getDisplayIconUrl(instance.icon_path)]),
|
||||
)
|
||||
currentLoadingBarIconUrls.value = Object.fromEntries(
|
||||
currentLoadingBars.value.map((bar) => {
|
||||
const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon)
|
||||
const profileIconUrl = bar.bar_type?.profile_path
|
||||
? profileIconUrls.get(bar.bar_type.profile_path)
|
||||
const instanceIconUrl = bar.bar_type?.instance_id
|
||||
? instanceIconUrls.get(bar.bar_type.instance_id)
|
||||
: null
|
||||
return [getLoadingBarKey(bar), barIconUrl ?? profileIconUrl ?? null]
|
||||
return [getLoadingBarKey(bar), barIconUrl ?? instanceIconUrl ?? null]
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -567,6 +474,12 @@ async function refreshLoadingBars() {
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
const installJobNotifications = await useInstallJobNotifications({
|
||||
router,
|
||||
handleError,
|
||||
onChange: updateNotification,
|
||||
})
|
||||
|
||||
await refreshLoadingBars()
|
||||
|
||||
const unlistenLoading = await loading_listener(async () => {
|
||||
@@ -588,20 +501,6 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('online', handleOnline)
|
||||
unlistenProcess()
|
||||
unlistenLoading()
|
||||
if (readyPillAnimationFrame !== null) {
|
||||
cancelAnimationFrame(readyPillAnimationFrame)
|
||||
}
|
||||
installJobNotifications.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.update-pill-ready-hidden {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.update-pill-ready-visible {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||
<hr v-if="option.type === 'divider'" class="divider" />
|
||||
<div
|
||||
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
|
||||
v-else-if="!(isInstanceLink(item) && option.name === `add_content`)"
|
||||
class="item clickable"
|
||||
:class="[option.color ?? 'base']"
|
||||
>
|
||||
@@ -64,10 +64,10 @@ defineExpose({
|
||||
},
|
||||
})
|
||||
|
||||
const isLinkedData = (item) => {
|
||||
if (item.instance != undefined && item.instance.linked_data) {
|
||||
const isInstanceLink = (item) => {
|
||||
if (item.instance != undefined && item.instance.link) {
|
||||
return true
|
||||
} else if (item != undefined && item.linked_data) {
|
||||
} else if (item != undefined && item.link) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ChatIcon } from '@/assets/icons'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { install } from '@/helpers/profile.js'
|
||||
import { install_existing_instance } from '@/helpers/install'
|
||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
@@ -72,7 +72,7 @@ defineExpose({
|
||||
title.value = 'No loader selected'
|
||||
errorType.value = 'no_loader_version'
|
||||
supportLink.value = 'https://support.modrinth.com'
|
||||
metadata.value.profilePath = context.profilePath
|
||||
metadata.value.instanceId = context.instanceId
|
||||
} else if (source === 'state_init') {
|
||||
title.value = 'Error initializing Modrinth App'
|
||||
errorType.value = 'state_init'
|
||||
@@ -125,7 +125,7 @@ const loadingRepair = ref(false)
|
||||
async function repairInstance() {
|
||||
loadingRepair.value = true
|
||||
try {
|
||||
await install(metadata.value.profilePath, false)
|
||||
await install_existing_instance(metadata.value.instanceId, false)
|
||||
errorModal.value.hide()
|
||||
} catch (err) {
|
||||
handleSevereError(err)
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<script setup>
|
||||
import { WrenchIcon, XIcon } from '@modrinth/assets'
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
FileTreeSelect,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { readDir, stat } from '@tauri-apps/plugin-fs'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
||||
import { PackageIcon } from '@/assets/icons'
|
||||
import {
|
||||
export_instance_mrpack,
|
||||
get_full_path,
|
||||
get_pack_export_candidates,
|
||||
} from '@/helpers/instance'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'app.export-modal.header', defaultMessage: 'Export modpack' },
|
||||
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack Name' },
|
||||
modpackNameLabel: { id: 'app.export-modal.modpack-name-label', defaultMessage: 'Modpack name' },
|
||||
modpackNamePlaceholder: {
|
||||
id: 'app.export-modal.modpack-name-placeholder',
|
||||
defaultMessage: 'Modpack name',
|
||||
@@ -39,15 +43,7 @@ const messages = defineMessages({
|
||||
id: 'app.export-modal.description-placeholder',
|
||||
defaultMessage: 'Enter modpack description...',
|
||||
},
|
||||
selectFilesLabel: {
|
||||
id: 'app.export-modal.select-files-label',
|
||||
defaultMessage: 'Configure which files are included in this export',
|
||||
},
|
||||
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||
includeFile: {
|
||||
id: 'app.export-modal.include-file-accessibility-label',
|
||||
defaultMessage: 'Include "{file}"?',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -59,8 +55,9 @@ const props = defineProps({
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
resetExportState()
|
||||
exportModal.value.show()
|
||||
initFiles()
|
||||
void initFiles().catch(handleError)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -69,62 +66,33 @@ const nameInput = ref(props.instance.name)
|
||||
const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const folders = ref([])
|
||||
const selectedFilePaths = ref([])
|
||||
const fileTreeKey = ref(0)
|
||||
const filesLoadId = ref(0)
|
||||
const instanceRoot = ref('')
|
||||
const loadedDirectories = ref(new Set())
|
||||
|
||||
const initFiles = async () => {
|
||||
const newFolders = new Map()
|
||||
const sep = '/'
|
||||
files.value = []
|
||||
await get_pack_export_candidates(props.instance.path).then((filePaths) =>
|
||||
filePaths
|
||||
.map((folder) => ({
|
||||
path: folder,
|
||||
name: folder.split(sep).pop(),
|
||||
selected:
|
||||
folder.startsWith('mods') ||
|
||||
folder.startsWith('datapacks') ||
|
||||
folder.startsWith('resourcepacks') ||
|
||||
folder.startsWith('shaderpacks') ||
|
||||
folder.startsWith('config'),
|
||||
disabled:
|
||||
folder === 'profile.json' ||
|
||||
folder.startsWith('modrinth_logs') ||
|
||||
folder.startsWith('.fabric') ||
|
||||
folder.startsWith('__MACOSX'),
|
||||
}))
|
||||
.forEach((pathData) => {
|
||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||
if (parent !== '') {
|
||||
if (newFolders.has(parent)) {
|
||||
newFolders.get(parent).push(pathData)
|
||||
} else {
|
||||
newFolders.set(parent, [pathData])
|
||||
}
|
||||
} else {
|
||||
files.value.push(pathData)
|
||||
}
|
||||
}),
|
||||
)
|
||||
folders.value = [...newFolders.entries()].map(([name, value]) => [
|
||||
{
|
||||
name,
|
||||
showingMore: false,
|
||||
},
|
||||
value,
|
||||
async function initFiles() {
|
||||
const loadId = ++filesLoadId.value
|
||||
const [filePaths, root] = await Promise.all([
|
||||
get_pack_export_candidates(props.instance.id),
|
||||
get_full_path(props.instance.id),
|
||||
])
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
instanceRoot.value = root
|
||||
const exportCandidates = await Promise.all(
|
||||
filePaths.map((path) => buildExportCandidateItem(root, path)),
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
files.value = exportCandidates
|
||||
selectedFilePaths.value = files.value
|
||||
.filter((file) => !file.disabled && isDefaultSelectedExportCandidate(file.path))
|
||||
.map((file) => file.path)
|
||||
}
|
||||
|
||||
await initFiles()
|
||||
|
||||
const exportPack = async () => {
|
||||
const filesToExport = files.value.filter((file) => file.selected).map((file) => file.path)
|
||||
folders.value.forEach((args) => {
|
||||
args[1].forEach((child) => {
|
||||
if (child.selected) {
|
||||
filesToExport.push(child.path)
|
||||
}
|
||||
})
|
||||
})
|
||||
const outputPath = await save({
|
||||
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
filters: [
|
||||
@@ -136,10 +104,10 @@ const exportPack = async () => {
|
||||
})
|
||||
|
||||
if (outputPath) {
|
||||
export_profile_mrpack(
|
||||
props.instance.path,
|
||||
export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
filesToExport,
|
||||
selectedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
@@ -147,94 +115,176 @@ const exportPack = async () => {
|
||||
exportModal.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
function resetExportState() {
|
||||
nameInput.value = props.instance.name
|
||||
exportDescription.value = ''
|
||||
versionInput.value = '1.0.0'
|
||||
files.value = []
|
||||
selectedFilePaths.value = []
|
||||
fileTreeKey.value += 1
|
||||
instanceRoot.value = ''
|
||||
loadedDirectories.value = new Set()
|
||||
}
|
||||
|
||||
async function loadExportDirectory(path) {
|
||||
if (!path || !instanceRoot.value || loadedDirectories.value.has(path)) return
|
||||
|
||||
const loadId = filesLoadId.value
|
||||
loadedDirectories.value.add(path)
|
||||
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot.value}/${path}`)
|
||||
const childItems = await Promise.all(
|
||||
entries.map((entry) => buildExportDirectoryChildItem(instanceRoot.value, path, entry)),
|
||||
)
|
||||
if (loadId !== filesLoadId.value) return
|
||||
|
||||
appendExportItems(childItems)
|
||||
} catch {
|
||||
loadedDirectories.value.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportCandidateItem(instanceRoot, path) {
|
||||
try {
|
||||
const entries = await readDir(`${instanceRoot}/${path}`)
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
count: entries.length,
|
||||
}
|
||||
} catch {
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
}
|
||||
|
||||
async function buildExportDirectoryChildItem(instanceRoot, parentPath, entry) {
|
||||
const path = `${parentPath}/${entry.name}`
|
||||
if (entry.isDirectory) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'directory',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
return buildExportFileItem(instanceRoot, path)
|
||||
}
|
||||
|
||||
async function buildExportFileItem(instanceRoot, path) {
|
||||
const metadata = await getExportCandidateMetadata(instanceRoot, path)
|
||||
return {
|
||||
path,
|
||||
type: 'file',
|
||||
disabled: isExportCandidateDisabled(path),
|
||||
size: metadata.size,
|
||||
modified: metadata.modified,
|
||||
}
|
||||
}
|
||||
|
||||
function appendExportItems(items) {
|
||||
const nextFiles = new Map(files.value.map((file) => [normalizeExportPath(file.path), file]))
|
||||
for (const item of items) {
|
||||
nextFiles.set(normalizeExportPath(item.path), item)
|
||||
}
|
||||
files.value = [...nextFiles.values()]
|
||||
}
|
||||
|
||||
async function getExportCandidateMetadata(instanceRoot, path) {
|
||||
try {
|
||||
const metadata = await stat(`${instanceRoot}/${path}`)
|
||||
return {
|
||||
size: metadata.size,
|
||||
modified: metadata.mtime ? Math.floor(metadata.mtime.getTime() / 1000) : undefined,
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExportPath(path) {
|
||||
return path.replaceAll('\\', '/').split('/').filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
function isDefaultSelectedExportCandidate(path) {
|
||||
return (
|
||||
path.startsWith('mods') ||
|
||||
path.startsWith('datapacks') ||
|
||||
path.startsWith('resourcepacks') ||
|
||||
path.startsWith('shaderpacks') ||
|
||||
path.startsWith('config')
|
||||
)
|
||||
}
|
||||
|
||||
function isExportCandidateDisabled(path) {
|
||||
return (
|
||||
path === 'profile.json' ||
|
||||
path.startsWith('modrinth_logs') ||
|
||||
path.startsWith('.fabric') ||
|
||||
path.startsWith('__MACOSX')
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
|
||||
<div class="flex flex-col gap-4 w-[40rem]">
|
||||
<NewModal
|
||||
ref="exportModal"
|
||||
:header="formatMessage(messages.header)"
|
||||
scrollable
|
||||
width="46rem"
|
||||
max-width="calc(100vw - 2rem)"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<div class="labeled_input w-full">
|
||||
<p class="text-contrast font-semibold">{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||
<div class="labeled_input w-full">
|
||||
<p class="text-contrast font-semibold">
|
||||
{{ formatMessage(messages.versionNumberLabel) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||
clearable
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||
<div class="flex flex-col gap-2 min-w-0">
|
||||
<p class="m-0 text-contrast font-semibold">
|
||||
{{ formatMessage(commonMessages.descriptionLabel) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Accordion
|
||||
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
|
||||
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
|
||||
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
Configure which files are included in this export
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
|
||||
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
|
||||
<Accordion
|
||||
class="flex flex-col"
|
||||
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
|
||||
>
|
||||
<template #title>
|
||||
<Checkbox
|
||||
:model-value="children.every((child) => child.selected)"
|
||||
:indeterminate="
|
||||
!children.every((child) => child.selected) &&
|
||||
children.some((child) => child.selected)
|
||||
"
|
||||
:description="formatMessage(messages.includeFile, { file: path.name })"
|
||||
class="pl-4 py-2"
|
||||
:disabled="children.every((x) => x.disabled)"
|
||||
@update:model-value="
|
||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||
"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
|
||||
</template>
|
||||
<div v-for="child in children" :key="child.path">
|
||||
<Checkbox
|
||||
v-model="child.selected"
|
||||
:label="child.name"
|
||||
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
|
||||
:disabled="child.disabled"
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
<Checkbox
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
v-model="file.selected"
|
||||
:label="file.name"
|
||||
:disabled="file.disabled"
|
||||
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
<FileTreeSelect
|
||||
:key="fileTreeKey"
|
||||
v-model="selectedFilePaths"
|
||||
class="min-w-0"
|
||||
:items="files"
|
||||
@navigate="loadExportDirectory"
|
||||
/>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="exportModal.hide">
|
||||
@@ -249,6 +299,6 @@ const exportPack = async () => {
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
@@ -15,9 +15,10 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { finish_install, kill, run } from '@/helpers/profile'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import { showInstanceInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
@@ -54,11 +55,11 @@ const installed = computed(() => props.instance.install_stage === 'installed')
|
||||
const router = useRouter()
|
||||
|
||||
const seeInstance = async () => {
|
||||
await router.push(`/instance/${encodeURIComponent(props.instance.path)}`)
|
||||
await router.push(`/instance/${encodeURIComponent(props.instance.id)}`)
|
||||
}
|
||||
|
||||
const checkProcess = async () => {
|
||||
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
||||
const runningProcesses = await get_by_instance_id(props.instance.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
@@ -66,8 +67,8 @@ const checkProcess = async () => {
|
||||
const play = async (e, context) => {
|
||||
e?.stopPropagation()
|
||||
loading.value = true
|
||||
await run(props.instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||
await run(props.instance.id)
|
||||
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
|
||||
.finally(() => {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: props.instance.loader,
|
||||
@@ -82,7 +83,7 @@ const stop = async (e, context) => {
|
||||
e?.stopPropagation()
|
||||
playing.value = false
|
||||
|
||||
await kill(props.instance.path).catch(handleError)
|
||||
await kill(props.instance.id).catch(handleError)
|
||||
|
||||
trackEvent('InstanceStop', {
|
||||
loader: props.instance.loader,
|
||||
@@ -94,17 +95,30 @@ const stop = async (e, context) => {
|
||||
const repair = async (e) => {
|
||||
e?.stopPropagation()
|
||||
|
||||
await finish_install(props.instance).catch(handleError)
|
||||
if (
|
||||
props.instance.install_stage !== 'pack_installed' &&
|
||||
(props.instance.link?.type === 'modrinth_modpack' ||
|
||||
props.instance.link?.type === 'server_project_modpack')
|
||||
) {
|
||||
await install_pack_to_existing_instance(props.instance.id, {
|
||||
type: 'fromVersionId',
|
||||
project_id: props.instance.link.project_id ?? props.instance.link.server_project_id ?? '',
|
||||
version_id: props.instance.link.version_id ?? props.instance.link.content_version_id ?? '',
|
||||
title: props.instance.name,
|
||||
}).catch(handleError)
|
||||
} else {
|
||||
await install_existing_instance(props.instance.id, false).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
const openFolder = async () => {
|
||||
await showProfileInFolder(props.instance.path)
|
||||
await showInstanceInFolder(props.instance.id)
|
||||
}
|
||||
|
||||
const addContent = async () => {
|
||||
await router.push({
|
||||
path: `/browse/${props.instance.loader === 'vanilla' ? 'datapack' : 'mod'}`,
|
||||
query: { i: props.instance.path },
|
||||
query: { i: props.instance.id },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -120,7 +134,7 @@ defineExpose({
|
||||
const currentEvent = ref(null)
|
||||
|
||||
const unlisten = await process_listener((e) => {
|
||||
if (e.profile_path_id === props.instance.path) {
|
||||
if (e.instance_id === props.instance.id) {
|
||||
currentEvent.value = e.event
|
||||
if (e.event === 'finished') {
|
||||
playing.value = false
|
||||
@@ -128,7 +142,9 @@ const unlisten = await process_listener((e) => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => checkProcess())
|
||||
onMounted(() => {
|
||||
checkProcess()
|
||||
})
|
||||
onUnmounted(() => unlisten())
|
||||
</script>
|
||||
|
||||
@@ -142,7 +158,7 @@ onUnmounted(() => unlisten())
|
||||
<Avatar
|
||||
size="48px"
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
alt="Mod card"
|
||||
/>
|
||||
<div class="h-full flex items-center font-bold text-contrast leading-normal">
|
||||
@@ -191,7 +207,7 @@ onUnmounted(() => unlisten())
|
||||
<Avatar
|
||||
size="48px"
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
alt="Mod card"
|
||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const instanceLink = computed(() => {
|
||||
const base = `/instance/${encodeURIComponent(props.instance.path)}`
|
||||
const base = `/instance/${encodeURIComponent(props.instance.id)}`
|
||||
return props.backTab ? `${base}/${props.backTab}` : base
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||
import { update_managed_modrinth_version } from '@/helpers/instance'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -32,14 +32,14 @@ const filteredVersions = computed(() => {
|
||||
})
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
const installedVersion = computed(() => props.instance?.linked_data?.version_id)
|
||||
const installedVersion = computed(() => props.instance?.link?.version_id)
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
const inProgress = ref(false)
|
||||
|
||||
const switchVersion = async (versionId) => {
|
||||
modpackVersionModal.value.hide()
|
||||
inProgress.value = true
|
||||
await update_managed_modrinth_version(props.instance.path, versionId)
|
||||
await update_managed_modrinth_version(props.instance.id, versionId)
|
||||
inProgress.value = false
|
||||
emit('finish-install')
|
||||
}
|
||||
@@ -59,7 +59,7 @@ const onHide = () => {
|
||||
:on-hide="onHide"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div v-if="instance.linked_data" class="mod-card">
|
||||
<div v-if="instance.link" class="mod-card">
|
||||
<div class="table">
|
||||
<div class="table-row with-columns table-head">
|
||||
<div class="table-cell table-text download-cell" />
|
||||
|
||||
@@ -6,16 +6,16 @@ import dayjs from 'dayjs'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { profile_listener } from '@/helpers/events.js'
|
||||
import { list } from '@/helpers/profile'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import { list } from '@/helpers/instance'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const recentInstances = ref([])
|
||||
const getInstances = async () => {
|
||||
const profiles = await list().catch(handleError)
|
||||
const instances = await list().catch(handleError)
|
||||
|
||||
recentInstances.value = profiles
|
||||
recentInstances.value = instances
|
||||
.sort((a, b) => {
|
||||
const dateACreated = dayjs(a.created)
|
||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||
@@ -37,24 +37,24 @@ const getInstances = async () => {
|
||||
|
||||
await getInstances()
|
||||
|
||||
const unlistenProfile = await profile_listener(async (event) => {
|
||||
const unlistenInstance = await instance_listener(async (event) => {
|
||||
if (event.event !== 'synced') {
|
||||
await getInstances()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
unlistenInstance()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="instance in recentInstances" :key="instance.id" v-tooltip.right="instance.name">
|
||||
<NavButton :to="`/instance/${encodeURIComponent(instance.path)}`" class="relative">
|
||||
<NavButton :to="`/instance/${encodeURIComponent(instance.id)}`" class="relative">
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
size="28px"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div
|
||||
@@ -65,10 +65,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</NavButton>
|
||||
</div>
|
||||
<div
|
||||
v-if="instances && recentInstances.length > 0"
|
||||
class="h-px w-6 mx-auto my-2 bg-divider"
|
||||
></div>
|
||||
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-divider"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
|
||||
<button
|
||||
v-if="showUpdatePill"
|
||||
type="button"
|
||||
class="!h-[34px] text-sm !transition-[opacity,transform,background-color,color,filter] !duration-200 ease-out"
|
||||
:class="{
|
||||
'opacity-0 scale-[0.96]': finishedDownloading && !animateReadyPill,
|
||||
'opacity-100 scale-100': finishedDownloading && animateReadyPill,
|
||||
}"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||
</span>
|
||||
<span v-else>{{ updateLabel }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, RefreshCwIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
appUpdateState,
|
||||
downloadAvailableAppUpdate,
|
||||
installAvailableAppUpdate,
|
||||
} from '@/providers/app-update'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
update: {
|
||||
id: 'app.action-bar.update',
|
||||
defaultMessage: 'Update',
|
||||
},
|
||||
downloadingUpdate: {
|
||||
id: 'app.action-bar.downloading-update',
|
||||
defaultMessage: 'Downloading update',
|
||||
},
|
||||
reloadToUpdate: {
|
||||
id: 'app.action-bar.reload-to-update',
|
||||
defaultMessage: 'Reload to update',
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
downloading,
|
||||
downloadPercent,
|
||||
downloadProgress,
|
||||
finishedDownloading,
|
||||
isVisible: isUpdateVisible,
|
||||
metered,
|
||||
restarting,
|
||||
} = appUpdateState
|
||||
|
||||
const isUpdateDownloading = computed(
|
||||
() =>
|
||||
downloading.value ||
|
||||
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
|
||||
)
|
||||
const showUpdatePill = computed(
|
||||
() => isUpdateVisible.value && (finishedDownloading.value || metered.value),
|
||||
)
|
||||
const animateReadyPill = ref(false)
|
||||
const updateLabel = computed(() => {
|
||||
if (isUpdateDownloading.value) {
|
||||
return formatMessage(messages.downloadingUpdate)
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
return formatMessage(messages.reloadToUpdate)
|
||||
}
|
||||
|
||||
return formatMessage(messages.update)
|
||||
})
|
||||
let readyPillAnimationFrame: number | null = null
|
||||
watch([showUpdatePill, finishedDownloading], async ([show, ready], [wasShown, wasReady]) => {
|
||||
if (readyPillAnimationFrame !== null) {
|
||||
cancelAnimationFrame(readyPillAnimationFrame)
|
||||
readyPillAnimationFrame = null
|
||||
}
|
||||
|
||||
if (!show || !ready) {
|
||||
animateReadyPill.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (wasShown && wasReady) {
|
||||
return
|
||||
}
|
||||
|
||||
animateReadyPill.value = false
|
||||
await nextTick()
|
||||
readyPillAnimationFrame = requestAnimationFrame(() => {
|
||||
animateReadyPill.value = true
|
||||
readyPillAnimationFrame = null
|
||||
})
|
||||
})
|
||||
async function handleUpdateClick() {
|
||||
if (isUpdateDownloading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
await installAvailableAppUpdate()
|
||||
} else {
|
||||
await downloadAvailableAppUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (readyPillAnimationFrame !== null) {
|
||||
cancelAnimationFrame(readyPillAnimationFrame)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { MailIcon, SearchIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
@@ -10,18 +10,12 @@ import {
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { friend_listener } from '@/helpers/events'
|
||||
import {
|
||||
add_friend,
|
||||
friends,
|
||||
type FriendWithUserData,
|
||||
remove_friend,
|
||||
transformFriends,
|
||||
} from '@/helpers/friends.ts'
|
||||
import { useFriends } from '@/composables/use-friends'
|
||||
import type { FriendWithUserData } from '@/helpers/friends.ts'
|
||||
import type { ModrinthCredentials } from '@/helpers/mr_auth'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -35,36 +29,23 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const userCredentials = computed(() => props.credentials)
|
||||
const {
|
||||
friends: userFriends,
|
||||
loading,
|
||||
requestFriend,
|
||||
acceptFriend,
|
||||
removeFriend: removeFriendRecord,
|
||||
} = useFriends({
|
||||
currentUserId: () => userCredentials.value?.user_id,
|
||||
getCredentials: () => userCredentials.value,
|
||||
onError: handleError,
|
||||
})
|
||||
|
||||
const search = ref('')
|
||||
const friendInvitesModal = ref()
|
||||
|
||||
const username = ref('')
|
||||
const addFriendModal = ref()
|
||||
async function addFriendFromModal() {
|
||||
addFriendModal.value.hide()
|
||||
await add_friend(username.value).catch(handleError)
|
||||
username.value = ''
|
||||
await loadFriends()
|
||||
}
|
||||
|
||||
async function addFriend(friend: FriendWithUserData) {
|
||||
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
|
||||
if (id) {
|
||||
await add_friend(id).catch(handleError)
|
||||
await loadFriends()
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFriend(friend: FriendWithUserData) {
|
||||
const id = friend.id === userCredentials.value?.user_id ? friend.friend_id : friend.id
|
||||
if (id) {
|
||||
await remove_friend(id).catch(handleError)
|
||||
await loadFriends()
|
||||
}
|
||||
}
|
||||
|
||||
const userFriends = ref<FriendWithUserData[]>([])
|
||||
const sortedFriends = computed<FriendWithUserData[]>(() =>
|
||||
userFriends.value.slice().sort((a, b) => {
|
||||
if (a.last_updated === null && b.last_updated === null) {
|
||||
@@ -108,42 +89,22 @@ const incomingRequests = computed(() =>
|
||||
.sort((a, b) => b.created.diff(a.created)),
|
||||
)
|
||||
|
||||
const loading = ref(true)
|
||||
async function loadFriends(timeout = false) {
|
||||
loading.value = timeout
|
||||
function addFriendFromModal() {
|
||||
const target = username.value.trim()
|
||||
if (!target) return
|
||||
|
||||
try {
|
||||
const friendsList = await friends()
|
||||
userFriends.value = await transformFriends(friendsList, userCredentials.value)
|
||||
loading.value = false
|
||||
} catch (e) {
|
||||
console.error('Error loading friends', e)
|
||||
if (timeout) {
|
||||
setTimeout(() => loadFriends(), 15 * 1000)
|
||||
}
|
||||
}
|
||||
addFriendModal.value.hide()
|
||||
requestFriend({ id: target, username: target })
|
||||
username.value = ''
|
||||
}
|
||||
|
||||
watch(
|
||||
userCredentials,
|
||||
() => {
|
||||
if (userCredentials.value === undefined) {
|
||||
userFriends.value = []
|
||||
loading.value = false
|
||||
} else if (userCredentials.value === null) {
|
||||
userFriends.value = []
|
||||
loading.value = false
|
||||
} else {
|
||||
loadFriends(true)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
function addFriend(friend: FriendWithUserData) {
|
||||
acceptFriend(friend)
|
||||
}
|
||||
|
||||
const unlisten = await friend_listener(() => loadFriends())
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
function removeFriend(friend: FriendWithUserData) {
|
||||
removeFriendRecord(friend)
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
addFriend: {
|
||||
@@ -301,11 +262,12 @@ const messages = defineMessages({
|
||||
</ButtonStyled>
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchFriends)"
|
||||
clearable
|
||||
variant="outlined"
|
||||
wrapper-class="flex-1"
|
||||
input-class="!bg-transparent !border !border-solid !border-button-bg !text-primary !placeholder:text-primary"
|
||||
wrapper-class="flex-1 [&>svg]:!text-primary [&>svg]:!opacity-100"
|
||||
@keyup.esc="search = ''"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -7,26 +7,28 @@ import {
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { list } from '@/helpers/profile'
|
||||
import { add_server_to_profile, get_profile_worlds } from '@/helpers/worlds.ts'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { add_server_to_instance, get_instance_worlds } from '@/helpers/worlds.ts'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const modal = ref()
|
||||
const searchFilter = ref('')
|
||||
const profiles = ref([])
|
||||
const instances = ref([])
|
||||
|
||||
const serverName = ref('')
|
||||
const serverAddress = ref('')
|
||||
|
||||
const shownProfiles = computed(() =>
|
||||
profiles.value.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
const shownInstances = computed(() =>
|
||||
instances.value.filter((instance) => {
|
||||
return instance.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -36,15 +38,15 @@ defineExpose({
|
||||
serverAddress.value = address
|
||||
searchFilter.value = ''
|
||||
|
||||
const profilesVal = await list().catch(handleError)
|
||||
const instanceValues = await list().catch(handleError)
|
||||
await Promise.allSettled(
|
||||
profilesVal.map(async (profile) => {
|
||||
profile.adding = false
|
||||
profile.added = false
|
||||
instanceValues.map(async (instance) => {
|
||||
instance.adding = false
|
||||
instance.added = false
|
||||
|
||||
try {
|
||||
const worlds = await get_profile_worlds(profile.path)
|
||||
profile.added = worlds.some(
|
||||
const worlds = await get_instance_worlds(instance.id)
|
||||
instance.added = worlds.some(
|
||||
(w) => w.type === 'server' && w.address === serverAddress.value,
|
||||
)
|
||||
} catch {
|
||||
@@ -53,28 +55,29 @@ defineExpose({
|
||||
}),
|
||||
)
|
||||
|
||||
profiles.value = profilesVal
|
||||
instances.value = instanceValues
|
||||
modal.value.show()
|
||||
|
||||
trackEvent('AddServerToInstanceStart', { source: 'AddServerToInstanceModal' })
|
||||
},
|
||||
})
|
||||
|
||||
async function addServer(profile) {
|
||||
profile.adding = true
|
||||
async function addServer(instance) {
|
||||
instance.adding = true
|
||||
try {
|
||||
await add_server_to_profile(profile.path, serverName.value, serverAddress.value, 'prompt')
|
||||
profile.added = true
|
||||
await add_server_to_instance(instance.id, serverName.value, serverAddress.value, 'prompt')
|
||||
instance.added = true
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.id] })
|
||||
|
||||
trackEvent('AddServerToInstance', {
|
||||
server_name: serverName.value,
|
||||
instance_name: profile.name,
|
||||
instance_name: instance.name,
|
||||
source: 'AddServerToInstanceModal',
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
profile.adding = false
|
||||
instance.adding = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -91,26 +94,26 @@ async function addServer(profile) {
|
||||
/>
|
||||
<div class="max-h-[21rem] overflow-y-auto">
|
||||
<div
|
||||
v-for="profile in shownProfiles"
|
||||
:key="profile.path"
|
||||
v-for="instance in shownInstances"
|
||||
:key="instance.id"
|
||||
class="flex w-full items-center justify-between gap-2 bg-bg-raised text-icon shadow-none"
|
||||
>
|
||||
<router-link
|
||||
class="btn btn-transparent p-2 text-left"
|
||||
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
||||
:to="`/instance/${encodeURIComponent(instance.id)}`"
|
||||
@click="modal.hide()"
|
||||
>
|
||||
<Avatar
|
||||
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
class="mr-2 [--size:2rem]"
|
||||
/>
|
||||
{{ profile.name }}
|
||||
{{ instance.name }}
|
||||
</router-link>
|
||||
<ButtonStyled>
|
||||
<button :disabled="profile.added || profile.adding" @click="addServer(profile)">
|
||||
<PlusIcon v-if="!profile.added && !profile.adding" />
|
||||
<CheckIcon v-else-if="profile.added" />
|
||||
{{ profile.adding ? 'Adding...' : profile.added ? 'Added' : 'Add' }}
|
||||
<button :disabled="instance.added || instance.adding" @click="addServer(instance)">
|
||||
<PlusIcon v-if="!instance.added && !instance.adding" />
|
||||
<CheckIcon v-else-if="instance.added" />
|
||||
{{ instance.adding ? 'Adding...' : instance.added ? 'Added' : 'Add' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<StackedAdmonitions v-bind="$attrs" :items="stackItems" class="w-full">
|
||||
<template #item="{ item, dismissible }">
|
||||
<InstanceAdmonitionsSharedInstanceStale
|
||||
v-if="item.kind === 'shared-instance-stale'"
|
||||
:instance="instance"
|
||||
@published="emit('published')"
|
||||
/>
|
||||
<InstanceAdmonitionsSharedInstanceWrongAccount
|
||||
v-else-if="item.kind === 'shared-instance-wrong-account'"
|
||||
:expected-user-id="sharedInstanceExpectedUserId"
|
||||
:role="sharedInstanceRole"
|
||||
:signed-out="sharedInstanceSignedOut"
|
||||
/>
|
||||
<InstanceAdmonitionsSharedInstanceUnavailable
|
||||
v-else-if="item.kind === 'shared-instance-unavailable'"
|
||||
:reason="sharedInstanceUnavailableReason"
|
||||
:manager="sharedInstanceUnavailableManager"
|
||||
:dismissible="dismissible"
|
||||
@dismiss="sharedInstanceUnavailableDismissed = true"
|
||||
/>
|
||||
</template>
|
||||
</StackedAdmonitions>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { StackedAdmonitions } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import type { SharedInstanceUnavailableReason } from '@/helpers/install'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
import InstanceAdmonitionsSharedInstanceStale from './instance-admonitions-shared-instance-stale.vue'
|
||||
import InstanceAdmonitionsSharedInstanceUnavailable from './instance-admonitions-shared-instance-unavailable.vue'
|
||||
import InstanceAdmonitionsSharedInstanceWrongAccount from './instance-admonitions-shared-instance-wrong-account.vue'
|
||||
import type { InstanceAdmonitionItem, SharedInstanceRole } from './types'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
sharedInstanceUnavailableReason?: SharedInstanceUnavailableReason | null
|
||||
sharedInstanceUnavailableManager?: string | null
|
||||
sharedInstanceWrongAccount?: boolean
|
||||
sharedInstanceExpectedUserId?: string | null
|
||||
sharedInstanceRole?: SharedInstanceRole | null
|
||||
sharedInstanceSignedOut?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
published: []
|
||||
}>()
|
||||
|
||||
const sharedInstanceWrongAccount = computed(() => props.sharedInstanceWrongAccount ?? false)
|
||||
const sharedInstanceUnavailableDismissed = ref(false)
|
||||
const showSharedInstancePublishAdmonition = computed(
|
||||
() =>
|
||||
!sharedInstanceWrongAccount.value &&
|
||||
props.instance.install_stage === 'installed' &&
|
||||
props.instance.shared_instance?.role === 'owner' &&
|
||||
props.instance.shared_instance.status === 'stale',
|
||||
)
|
||||
|
||||
const stackItems = computed<InstanceAdmonitionItem[]>(() => {
|
||||
const items: InstanceAdmonitionItem[] = []
|
||||
|
||||
if (sharedInstanceWrongAccount.value) {
|
||||
items.push({
|
||||
id: 'shared-instance-wrong-account',
|
||||
type: 'warning',
|
||||
dismissible: false,
|
||||
kind: 'shared-instance-wrong-account',
|
||||
})
|
||||
}
|
||||
|
||||
if (props.sharedInstanceUnavailableReason && !sharedInstanceUnavailableDismissed.value) {
|
||||
items.push({
|
||||
id: 'shared-instance-unavailable',
|
||||
type: 'warning',
|
||||
dismissible: true,
|
||||
kind: 'shared-instance-unavailable',
|
||||
})
|
||||
}
|
||||
|
||||
if (showSharedInstancePublishAdmonition.value) {
|
||||
items.push({
|
||||
id: 'shared-instance-stale',
|
||||
type: 'warning',
|
||||
dismissible: false,
|
||||
kind: 'shared-instance-stale',
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.instance.id, props.sharedInstanceUnavailableReason],
|
||||
() => {
|
||||
sharedInstanceUnavailableDismissed.value = false
|
||||
},
|
||||
)
|
||||
</script>
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { defineMessages } from '@modrinth/ui'
|
||||
|
||||
export const instanceAdmonitionsMessages = defineMessages({
|
||||
sharedInstanceChangesHeader: {
|
||||
id: 'app.instance.admonitions.shared-instance.changes-header',
|
||||
defaultMessage: "Your changes haven't been shared yet",
|
||||
},
|
||||
sharedInstanceChangesBody: {
|
||||
id: 'app.instance.admonitions.shared-instance.changes-body',
|
||||
defaultMessage: "Your local instance is ahead of the users you've shared it with.",
|
||||
},
|
||||
sharedInstancePublishButton: {
|
||||
id: 'app.instance.admonitions.shared-instance.publish-button',
|
||||
defaultMessage: 'Push update',
|
||||
},
|
||||
sharedInstancePublishingButton: {
|
||||
id: 'app.instance.admonitions.shared-instance.publishing-button',
|
||||
defaultMessage: 'Pushing...',
|
||||
},
|
||||
sharedInstanceReviewingButton: {
|
||||
id: 'app.instance.admonitions.shared-instance.reviewing-button',
|
||||
defaultMessage: 'Reviewing...',
|
||||
},
|
||||
sharedInstanceReviewHeader: {
|
||||
id: 'app.instance.admonitions.shared-instance.review-header',
|
||||
defaultMessage: 'Review changes',
|
||||
},
|
||||
sharedInstanceReviewAdmonitionHeader: {
|
||||
id: 'app.instance.admonitions.shared-instance.review-admonition-header',
|
||||
defaultMessage: 'Push update',
|
||||
},
|
||||
sharedInstanceReviewDescription: {
|
||||
id: 'app.instance.admonitions.shared-instance.review-description',
|
||||
defaultMessage:
|
||||
'Review the content changes that will be shared with everyone using this instance.',
|
||||
},
|
||||
sharedInstanceAddedLabel: {
|
||||
id: 'app.instance.admonitions.shared-instance.added-label',
|
||||
defaultMessage: 'Added',
|
||||
},
|
||||
sharedInstanceRemovedLabel: {
|
||||
id: 'app.instance.admonitions.shared-instance.removed-label',
|
||||
defaultMessage: 'Removed',
|
||||
},
|
||||
sharedInstanceWrongAccountHeader: {
|
||||
id: 'app.instance.shared-instance-wrong-account.warning-header',
|
||||
defaultMessage: 'You are using the wrong Modrinth account',
|
||||
},
|
||||
sharedInstanceSignedOutHeader: {
|
||||
id: 'app.instance.shared-instance-wrong-account.signed-out-header',
|
||||
defaultMessage: 'You need to sign in to Modrinth',
|
||||
},
|
||||
sharedInstanceWrongAccountSignInAs: {
|
||||
id: 'app.instance.shared-instance-wrong-account.sign-in-as-label',
|
||||
defaultMessage: 'Sign in as',
|
||||
},
|
||||
sharedInstanceWrongAccountUserBody: {
|
||||
id: 'app.instance.shared-instance-wrong-account.user-admonition-body-v2',
|
||||
defaultMessage: 'to receive updates for this shared instance.',
|
||||
},
|
||||
sharedInstanceWrongAccountOwnerBody: {
|
||||
id: 'app.instance.shared-instance-wrong-account.owner-admonition-body-v2',
|
||||
defaultMessage: "to manage this shared instance. You won't be able to push updates to users.",
|
||||
},
|
||||
sharedInstanceWrongAccountFallbackUsername: {
|
||||
id: 'app.instance.shared-instance-wrong-account.fallback-username',
|
||||
defaultMessage: 'the linked account',
|
||||
},
|
||||
})
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<Admonition
|
||||
type="warning"
|
||||
inline-actions
|
||||
:header="formatMessage(messages.sharedInstanceChangesHeader)"
|
||||
>
|
||||
{{ formatMessage(messages.sharedInstanceChangesBody) }}
|
||||
<template #actions>
|
||||
<ButtonStyled color="orange">
|
||||
<button class="!h-10" :disabled="isPublishButtonDisabled" @click="reviewChanges">
|
||||
<UploadIcon aria-hidden="true" />
|
||||
{{
|
||||
isPublishing
|
||||
? formatMessage(messages.sharedInstancePublishingButton)
|
||||
: isReviewingPublish
|
||||
? formatMessage(messages.sharedInstanceReviewingButton)
|
||||
: formatMessage(messages.sharedInstancePublishButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</Admonition>
|
||||
|
||||
<ContentDiffModal
|
||||
ref="publishReviewModal"
|
||||
:header="formatMessage(messages.sharedInstanceReviewHeader)"
|
||||
:admonition-header="formatMessage(messages.sharedInstanceReviewAdmonitionHeader)"
|
||||
:description="formatMessage(messages.sharedInstanceReviewDescription)"
|
||||
:diffs="publishDiffs"
|
||||
:confirm-label="formatMessage(messages.sharedInstancePublishButton)"
|
||||
:confirm-icon="UploadIcon"
|
||||
:added-label="formatMessage(messages.sharedInstanceAddedLabel)"
|
||||
:removed-label="formatMessage(messages.sharedInstanceRemovedLabel)"
|
||||
@confirm="publishChanges"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UploadIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
type ContentDiffItem,
|
||||
ContentDiffModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
getSharedInstanceUnavailableReason,
|
||||
isSharedInstanceUnavailableError,
|
||||
} from '@/helpers/install'
|
||||
import { get_shared_instance_publish_preview, publish_shared_instance } from '@/helpers/instance'
|
||||
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages'
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
published: []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { notifySharedInstanceError, notifySharedInstanceUnavailable } = useSharedInstanceErrors()
|
||||
const isPublishing = ref(false)
|
||||
const isReviewingPublish = ref(false)
|
||||
const publishReviewModal = ref<InstanceType<typeof ContentDiffModal>>()
|
||||
const publishDiffs = ref<ContentDiffItem[]>([])
|
||||
|
||||
const isPublishButtonDisabled = computed(() => isPublishing.value || isReviewingPublish.value)
|
||||
|
||||
async function reviewChanges(e?: MouseEvent) {
|
||||
if (isPublishButtonDisabled.value) return
|
||||
|
||||
isReviewingPublish.value = true
|
||||
try {
|
||||
const preview = await get_shared_instance_publish_preview(props.instance.id)
|
||||
if (!preview) {
|
||||
notifySharedInstanceUnavailable()
|
||||
emit('published')
|
||||
return
|
||||
}
|
||||
if (preview.diffs.length === 0) {
|
||||
emit('published')
|
||||
return
|
||||
}
|
||||
|
||||
publishDiffs.value = preview.diffs.map((diff) => ({
|
||||
type: diff.type,
|
||||
projectName: diff.projectName ?? undefined,
|
||||
fileName: diff.fileName ?? undefined,
|
||||
currentVersionName: diff.currentVersionName ?? undefined,
|
||||
newVersionName: diff.newVersionName ?? undefined,
|
||||
disabled: diff.disabled,
|
||||
}))
|
||||
publishReviewModal.value?.show(e)
|
||||
} catch (err) {
|
||||
if (isSharedInstanceUnavailableError(err)) {
|
||||
notifySharedInstanceUnavailable(getSharedInstanceUnavailableReason(err))
|
||||
emit('published')
|
||||
return
|
||||
}
|
||||
|
||||
notifySharedInstanceError(err)
|
||||
} finally {
|
||||
isReviewingPublish.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function publishChanges() {
|
||||
if (isPublishing.value) return
|
||||
|
||||
isPublishing.value = true
|
||||
try {
|
||||
await publish_shared_instance(props.instance.id)
|
||||
emit('published')
|
||||
} catch (err) {
|
||||
if (isSharedInstanceUnavailableError(err)) {
|
||||
notifySharedInstanceUnavailable(getSharedInstanceUnavailableReason(err))
|
||||
emit('published')
|
||||
return
|
||||
}
|
||||
|
||||
notifySharedInstanceError(err)
|
||||
} finally {
|
||||
isPublishing.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<Admonition
|
||||
type="warning"
|
||||
:header="formatMessage(sharedInstanceErrorMessages.unavailableTitle)"
|
||||
:dismissible="dismissible"
|
||||
@dismiss="emit('dismiss')"
|
||||
>
|
||||
{{ formatSharedInstanceUnavailable(reason ?? null, manager) }}
|
||||
</Admonition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Admonition, useVIntl } from '@modrinth/ui'
|
||||
|
||||
import type { SharedInstanceUnavailableReason } from '@/helpers/install'
|
||||
import {
|
||||
sharedInstanceErrorMessages,
|
||||
useSharedInstanceErrors,
|
||||
} from '@/helpers/shared-instance-errors'
|
||||
|
||||
defineProps<{
|
||||
reason?: SharedInstanceUnavailableReason | null
|
||||
manager?: string | null
|
||||
dismissible?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
dismiss: []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { formatSharedInstanceUnavailable } = useSharedInstanceErrors()
|
||||
</script>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<Admonition type="warning" :header="formatMessage(headerMessage)">
|
||||
<span class="flex flex-wrap items-center gap-x-1.5 gap-y-1">
|
||||
<span>{{ formatMessage(messages.sharedInstanceWrongAccountSignInAs) }}</span>
|
||||
<span
|
||||
v-if="sharedInstanceExpectedUser"
|
||||
class="inline-flex max-w-full min-w-0 items-center gap-1.5 align-middle font-semibold text-contrast"
|
||||
>
|
||||
<Avatar
|
||||
:src="sharedInstanceExpectedUser.avatarUrl"
|
||||
:alt="sharedInstanceExpectedUsername"
|
||||
:tint-by="sharedInstanceExpectedUser.tintBy"
|
||||
size="20px"
|
||||
circle
|
||||
no-shadow
|
||||
/>
|
||||
<span class="min-w-0 truncate">{{ sharedInstanceExpectedUsername }}</span>
|
||||
</span>
|
||||
<span v-else class="font-semibold">{{ sharedInstanceExpectedUsername }}</span>
|
||||
<span>{{ formatMessage(bodyMessage) }}</span>
|
||||
</span>
|
||||
</Admonition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Admonition, Avatar, useVIntl } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { get_user } from '@/helpers/cache'
|
||||
|
||||
import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages'
|
||||
import type { SharedInstanceRole } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
expectedUserId?: string | null
|
||||
role?: SharedInstanceRole | null
|
||||
signedOut?: boolean
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const expectedUserId = computed(() => props.expectedUserId ?? null)
|
||||
const expectedUserQuery = useQuery({
|
||||
queryKey: computed(() => ['user', expectedUserId.value]),
|
||||
queryFn: async () => {
|
||||
if (!expectedUserId.value) return null
|
||||
|
||||
return await get_user(expectedUserId.value, 'bypass').catch(() => null)
|
||||
},
|
||||
enabled: () => !!expectedUserId.value,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const sharedInstanceExpectedUser = computed(() => {
|
||||
const user = expectedUserQuery.data.value
|
||||
if (!user) return null
|
||||
|
||||
return {
|
||||
username: user.username,
|
||||
avatarUrl: user.avatar_url ?? undefined,
|
||||
tintBy: user.id,
|
||||
}
|
||||
})
|
||||
const sharedInstanceExpectedUsername = computed(
|
||||
() =>
|
||||
sharedInstanceExpectedUser.value?.username ||
|
||||
formatMessage(messages.sharedInstanceWrongAccountFallbackUsername),
|
||||
)
|
||||
const headerMessage = computed(() =>
|
||||
props.signedOut
|
||||
? messages.sharedInstanceSignedOutHeader
|
||||
: messages.sharedInstanceWrongAccountHeader,
|
||||
)
|
||||
const bodyMessage = computed(() =>
|
||||
props.role === 'owner'
|
||||
? messages.sharedInstanceWrongAccountOwnerBody
|
||||
: messages.sharedInstanceWrongAccountUserBody,
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { StackedAdmonitionItem } from '@modrinth/ui'
|
||||
|
||||
export type InstanceAdmonitionKind =
|
||||
| 'shared-instance-stale'
|
||||
| 'shared-instance-unavailable'
|
||||
| 'shared-instance-wrong-account'
|
||||
|
||||
export type InstanceAdmonitionItem = StackedAdmonitionItem & {
|
||||
kind: InstanceAdmonitionKind
|
||||
}
|
||||
|
||||
export type SharedInstanceRole = 'owner' | 'member'
|
||||
@@ -19,7 +19,8 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import ConfirmDeleteInstanceModal from '@/components/ui/modal/ConfirmDeleteInstanceModal.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||
import { install_duplicate_instance } from '@/helpers/install'
|
||||
import { edit, edit_icon, list, remove } from '@/helpers/instance'
|
||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||
|
||||
import type { GameInstance } from '../../../helpers/types'
|
||||
@@ -32,14 +33,14 @@ const queryClient = useQueryClient()
|
||||
const deleteConfirmModal = ref()
|
||||
|
||||
const { instance } = injectInstanceSettings()
|
||||
type ReleaseChannel = GameInstance['preferred_update_channel']
|
||||
type ReleaseChannel = GameInstance['update_channel']
|
||||
const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
|
||||
|
||||
const title = ref(instance.value.name)
|
||||
const icon: Ref<string | undefined> = ref(instance.value.icon_path)
|
||||
const groups = ref([...instance.value.groups])
|
||||
const savingReleaseChannel = ref(false)
|
||||
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.preferred_update_channel)
|
||||
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.update_channel)
|
||||
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
|
||||
savingReleaseChannel.value ? [...releaseChannelOptions] : [],
|
||||
)
|
||||
@@ -48,8 +49,8 @@ const newCategoryInput = ref('')
|
||||
|
||||
const installing = computed(() => instance.value.install_stage !== 'installed')
|
||||
|
||||
async function duplicateProfile() {
|
||||
await duplicate(instance.value.path).catch(handleError)
|
||||
async function duplicateInstance() {
|
||||
await install_duplicate_instance(instance.value.id).catch(handleError)
|
||||
trackEvent('InstanceDuplicate', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
@@ -84,22 +85,22 @@ function formatReleaseChannelDescription(channel: ReleaseChannel) {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [instance.value.path, instance.value.preferred_update_channel] as const,
|
||||
() => [instance.value.id, instance.value.update_channel] as const,
|
||||
() => {
|
||||
if (!savingReleaseChannel.value) {
|
||||
selectedReleaseChannel.value = instance.value.preferred_update_channel
|
||||
selectedReleaseChannel.value = instance.value.update_channel
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(selectedReleaseChannel, async (channel, previousChannel) => {
|
||||
const previousReleaseChannel = previousChannel ?? instance.value.preferred_update_channel
|
||||
if (channel === instance.value.preferred_update_channel) return
|
||||
const previousReleaseChannel = previousChannel ?? instance.value.update_channel
|
||||
if (channel === instance.value.update_channel) return
|
||||
|
||||
savingReleaseChannel.value = true
|
||||
const profilePath = instance.value.path
|
||||
await edit(profilePath, { preferred_update_channel: channel })
|
||||
.then(() => queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', profilePath] }))
|
||||
const instanceId = instance.value.id
|
||||
await edit(instanceId, { update_channel: channel })
|
||||
.then(() => queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instanceId] }))
|
||||
.catch((error) => {
|
||||
selectedReleaseChannel.value = previousReleaseChannel
|
||||
handleError(error)
|
||||
@@ -109,7 +110,7 @@ watch(selectedReleaseChannel, async (channel, previousChannel) => {
|
||||
|
||||
async function resetIcon() {
|
||||
icon.value = undefined
|
||||
await edit_icon(instance.value.path, null).catch(handleError)
|
||||
await edit_icon(instance.value.id, null).catch(handleError)
|
||||
trackEvent('InstanceRemoveIcon')
|
||||
}
|
||||
|
||||
@@ -127,12 +128,12 @@ async function setIcon() {
|
||||
if (!value) return
|
||||
|
||||
icon.value = value
|
||||
await edit_icon(instance.value.path, icon.value).catch(handleError)
|
||||
await edit_icon(instance.value.id, icon.value).catch(handleError)
|
||||
|
||||
trackEvent('InstanceSetIcon')
|
||||
}
|
||||
|
||||
const editProfileObject = computed(() => ({
|
||||
const editInstanceObject = computed(() => ({
|
||||
name: title.value.trim().substring(0, 32) ?? 'Instance',
|
||||
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
|
||||
}))
|
||||
@@ -158,15 +159,15 @@ watch(
|
||||
[title, groups, groups],
|
||||
async () => {
|
||||
if (removing.value) return
|
||||
await edit(instance.value.path, editProfileObject.value).catch(handleError)
|
||||
await edit(instance.value.id, editInstanceObject.value).catch(handleError)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const removing = ref(false)
|
||||
async function removeProfile() {
|
||||
async function removeInstance() {
|
||||
removing.value = true
|
||||
const path = instance.value.path
|
||||
const path = instance.value.id
|
||||
|
||||
trackEvent('InstanceRemove', {
|
||||
loader: instance.value.loader,
|
||||
@@ -284,7 +285,7 @@ const messages = defineMessages({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="removeProfile" />
|
||||
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="removeInstance" />
|
||||
<div class="block">
|
||||
<div class="float-end ml-10 relative group w-fit">
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -310,7 +311,7 @@ const messages = defineMessages({
|
||||
:src="icon ? convertFileSrc(icon) : icon"
|
||||
size="108px"
|
||||
class="transition-[filter] group-hover:brightness-75"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
no-shadow
|
||||
/>
|
||||
<div
|
||||
@@ -350,7 +351,7 @@ const messages = defineMessages({
|
||||
aria-labelledby="duplicate-instance-label"
|
||||
:disabled="installing"
|
||||
class="w-max !shadow-none"
|
||||
@click="duplicateProfile"
|
||||
@click="duplicateInstance"
|
||||
>
|
||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||
</button>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { edit } from '@/helpers/profile'
|
||||
import { edit } from '@/helpers/instance'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||
|
||||
@@ -28,21 +28,21 @@ const overrideHooks = ref(
|
||||
)
|
||||
const hooks = ref(instance.value.hooks ?? globalSettings.hooks)
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
const editInstanceObject = computed(() => {
|
||||
const editInstancePatch: {
|
||||
hooks?: Hooks
|
||||
} = {}
|
||||
|
||||
// When hooks are not overridden per-instance, we want to clear them
|
||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||
editInstancePatch.hooks = overrideHooks.value ? hooks.value : {}
|
||||
|
||||
return editProfile
|
||||
return editInstancePatch
|
||||
})
|
||||
|
||||
watch(
|
||||
[overrideHooks, hooks],
|
||||
async () => {
|
||||
await edit(instance.value.path, editProfileObject.value)
|
||||
await edit(instance.value.id, editInstanceObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
formatLoaderLabel,
|
||||
injectFilePicker,
|
||||
injectNotificationManager,
|
||||
InstallationSettingsLayout,
|
||||
provideAppBackup,
|
||||
provideInstallationSettings,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
@@ -15,32 +15,46 @@ import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue'
|
||||
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_versions, get_version } from '@/helpers/cache'
|
||||
import { get_loader_versions } from '@/helpers/metadata'
|
||||
import {
|
||||
duplicate,
|
||||
install_existing_instance,
|
||||
install_pack_to_existing_instance,
|
||||
wait_for_install_job,
|
||||
} from '@/helpers/install'
|
||||
import {
|
||||
edit,
|
||||
get_linked_modpack_info,
|
||||
install,
|
||||
list,
|
||||
unlink_shared_instance,
|
||||
unpublish_shared_instance,
|
||||
update_managed_modrinth_version,
|
||||
update_repair_modrinth,
|
||||
} from '@/helpers/profile'
|
||||
} from '@/helpers/instance'
|
||||
import { get_loader_versions } from '@/helpers/metadata'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { provideInstanceBackup } from '@/providers/instance-backup'
|
||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import type { Manifest } from '../../../helpers/types'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const filePicker = injectFilePicker()
|
||||
const { formatMessage } = useVIntl()
|
||||
const queryClient = useQueryClient()
|
||||
const debug = useDebugLogger('AppInstallationSettings')
|
||||
const themeStore = useTheming()
|
||||
|
||||
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
|
||||
const managedContentPolicy = useManagedContentPolicy(instance)
|
||||
const skipNonEssentialWarnings = computed(() =>
|
||||
themeStore.getFeatureFlag('skip_non_essential_warnings'),
|
||||
)
|
||||
|
||||
debug('metadata load: start', {
|
||||
instancePath: instance.value.path,
|
||||
instanceId: instance.value.id,
|
||||
loader: instance.value.loader,
|
||||
gameVersion: instance.value.game_version,
|
||||
installStage: instance.value.install_stage,
|
||||
@@ -91,20 +105,63 @@ const metadataLoading = computed(() =>
|
||||
)
|
||||
|
||||
debug('metadata queries configured', {
|
||||
instancePath: instance.value.path,
|
||||
instanceId: instance.value.id,
|
||||
loader: instance.value.loader,
|
||||
gameVersion: instance.value.game_version,
|
||||
})
|
||||
|
||||
const isModrinthLinkedModpack = computed(
|
||||
() =>
|
||||
instance.value.link?.type === 'modrinth_modpack' ||
|
||||
instance.value.link?.type === 'server_project_modpack' ||
|
||||
(instance.value.link?.type === 'shared_instance' &&
|
||||
!!instance.value.link.modpack_project_id &&
|
||||
!!instance.value.link.modpack_version_id),
|
||||
)
|
||||
const isImportedModpack = computed(() => instance.value.link?.type === 'imported_modpack')
|
||||
const isSharedInstanceManagedModpack = managedContentPolicy.isManagedModpack
|
||||
const canUnpublishSharedInstance = managedContentPolicy.canUnpublish
|
||||
const canUnlinkSharedInstance = managedContentPolicy.canUnlink
|
||||
|
||||
const modpackInfoQuery = useQuery({
|
||||
queryKey: computed(() => ['linkedModpackInfo', instance.value.path]),
|
||||
queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'),
|
||||
enabled: computed(() => !!instance.value.linked_data?.project_id && !offline),
|
||||
queryKey: computed(() => ['linkedModpackInfo', instance.value.id]),
|
||||
queryFn: () => get_linked_modpack_info(instance.value.id, 'must_revalidate'),
|
||||
enabled: computed(() => isModrinthLinkedModpack.value && !offline),
|
||||
})
|
||||
const modpackInfo = modpackInfoQuery.data
|
||||
|
||||
const repairing = ref(false)
|
||||
const reinstalling = ref(false)
|
||||
const unpublishingSharedInstance = ref(false)
|
||||
const unlinkingSharedInstance = ref(false)
|
||||
|
||||
async function unpublishSharedInstance() {
|
||||
unpublishingSharedInstance.value = true
|
||||
try {
|
||||
await unpublish_shared_instance(instance.value.id)
|
||||
await queryClient.invalidateQueries({ queryKey: ['sharedInstanceUsers', instance.value.id] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] })
|
||||
onUnlinked()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
unpublishingSharedInstance.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkSharedInstance() {
|
||||
unlinkingSharedInstance.value = true
|
||||
try {
|
||||
await unlink_shared_instance(instance.value.id)
|
||||
await queryClient.invalidateQueries({ queryKey: ['sharedInstanceUsers', instance.value.id] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] })
|
||||
onUnlinked()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
unlinkingSharedInstance.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
loaderVersion: {
|
||||
@@ -129,24 +186,21 @@ function getManifest(loader: string) {
|
||||
return manifest
|
||||
}
|
||||
|
||||
provideAppBackup({
|
||||
async createBackup() {
|
||||
debug('createBackup: start', {
|
||||
instancePath: instance.value.path,
|
||||
instanceName: instance.value.name,
|
||||
})
|
||||
const allProfiles = await list()
|
||||
const prefix = `${instance.value.name} - Backup #`
|
||||
const existingNums = allProfiles
|
||||
.filter((p) => p.name.startsWith(prefix))
|
||||
.map((p) => parseInt(p.name.slice(prefix.length), 10))
|
||||
.filter((n) => !isNaN(n))
|
||||
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
|
||||
const newPath = await duplicate(instance.value.path)
|
||||
await edit(newPath, { name: `${prefix}${nextNum}` })
|
||||
debug('createBackup: done', { newPath, backupName: `${prefix}${nextNum}` })
|
||||
},
|
||||
})
|
||||
async function installLocalModpackFromPicker() {
|
||||
const picked = await filePicker.pickModpackFile({ readFile: false })
|
||||
if (!picked?.path) return false
|
||||
|
||||
const job = await install_pack_to_existing_instance(instance.value.id, {
|
||||
type: 'fromFile',
|
||||
path: picked.path,
|
||||
}).catch(handleError)
|
||||
if (!job) return false
|
||||
|
||||
const completed = await wait_for_install_job(job.job_id).catch(handleError)
|
||||
return !!completed
|
||||
}
|
||||
|
||||
provideInstanceBackup(instance)
|
||||
|
||||
provideInstallationSettings({
|
||||
closeSettings: closeModal,
|
||||
@@ -172,15 +226,31 @@ provideInstallationSettings({
|
||||
}
|
||||
return rows
|
||||
}),
|
||||
isLinked: computed(() => !!instance.value.linked_data?.locked),
|
||||
isLinked: computed(
|
||||
() =>
|
||||
isModrinthLinkedModpack.value ||
|
||||
isImportedModpack.value ||
|
||||
isSharedInstanceManagedModpack.value,
|
||||
),
|
||||
isBusy: computed(
|
||||
() =>
|
||||
instance.value.install_stage !== 'installed' ||
|
||||
repairing.value ||
|
||||
reinstalling.value ||
|
||||
unlinkingSharedInstance.value ||
|
||||
unpublishingSharedInstance.value ||
|
||||
!!offline,
|
||||
),
|
||||
skipNonEssentialWarnings,
|
||||
modpack: computed(() => {
|
||||
if (isImportedModpack.value && instance.value.link?.type === 'imported_modpack') {
|
||||
return {
|
||||
iconUrl: instance.value.icon_path,
|
||||
title: instance.value.link.name ?? instance.value.name,
|
||||
versionNumber: instance.value.link.version_number ?? undefined,
|
||||
filename: instance.value.link.filename ?? undefined,
|
||||
}
|
||||
}
|
||||
if (!modpackInfo.value) return null
|
||||
return {
|
||||
iconUrl: modpackInfo.value.project.icon_url,
|
||||
@@ -224,16 +294,31 @@ provideInstallationSettings({
|
||||
debug('resolveLoaderVersions: no manifest', { loader, gameVersion })
|
||||
return []
|
||||
}
|
||||
if (loader === 'fabric' || loader === 'quilt') {
|
||||
const result = manifest.gameVersions[0]?.loaders ?? []
|
||||
debug('resolveLoaderVersions: fabric/quilt result', {
|
||||
const entry = manifest.gameVersions?.find((item) => item.id === gameVersion)
|
||||
if (entry?.versionGroup) {
|
||||
const result =
|
||||
manifest.versionGroups?.find((group) => group.id === entry.versionGroup)?.loaders ?? []
|
||||
debug('resolveLoaderVersions: version group result', {
|
||||
loader,
|
||||
gameVersion,
|
||||
versionGroup: entry.versionGroup,
|
||||
count: result.length,
|
||||
})
|
||||
return result
|
||||
}
|
||||
const placeholder = manifest.gameVersions?.find((item) => item.id === '${modrinth.gameVersion}')
|
||||
if (placeholder) {
|
||||
const result = manifest.gameVersions?.some((item) => item.id === gameVersion)
|
||||
? placeholder.loaders
|
||||
: []
|
||||
debug('resolveLoaderVersions: placeholder result', {
|
||||
loader,
|
||||
gameVersion,
|
||||
count: result.length,
|
||||
})
|
||||
return result
|
||||
}
|
||||
const result = manifest.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
|
||||
const result = entry?.loaders ?? []
|
||||
debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length })
|
||||
return result
|
||||
},
|
||||
@@ -261,25 +346,25 @@ provideInstallationSettings({
|
||||
|
||||
async save(platform, gameVersion, loaderVersionId) {
|
||||
debug('save: called', {
|
||||
instancePath: instance.value.path,
|
||||
instanceId: instance.value.id,
|
||||
platform,
|
||||
gameVersion,
|
||||
loaderVersionId,
|
||||
})
|
||||
const editProfile: Record<string, string | undefined> = {
|
||||
const editInstancePatch: Record<string, string | undefined> = {
|
||||
loader: platform,
|
||||
game_version: gameVersion,
|
||||
}
|
||||
if (platform !== 'vanilla' && loaderVersionId) {
|
||||
editProfile.loader_version = loaderVersionId
|
||||
editInstancePatch.loader_version = loaderVersionId
|
||||
}
|
||||
await edit(instance.value.path, editProfile).catch(handleError)
|
||||
debug('save: edit complete', { editProfile })
|
||||
await edit(instance.value.id, editInstancePatch).catch(handleError)
|
||||
debug('save: edit complete', { editInstancePatch })
|
||||
},
|
||||
|
||||
afterSave: async () => {
|
||||
debug('afterSave: installing', { instancePath: instance.value.path })
|
||||
await install(instance.value.path, false).catch(handleError)
|
||||
debug('afterSave: installing', { instanceId: instance.value.id })
|
||||
await install_existing_instance(instance.value.id, false).catch(handleError)
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
@@ -288,9 +373,9 @@ provideInstallationSettings({
|
||||
},
|
||||
|
||||
async repair() {
|
||||
debug('repair: called', { instancePath: instance.value.path })
|
||||
debug('repair: called', { instanceId: instance.value.id })
|
||||
repairing.value = true
|
||||
await install(instance.value.path, true).catch(handleError)
|
||||
await install_existing_instance(instance.value.id, true).catch(handleError)
|
||||
repairing.value = false
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: instance.value.loader,
|
||||
@@ -300,24 +385,52 @@ provideInstallationSettings({
|
||||
},
|
||||
|
||||
async reinstallModpack() {
|
||||
debug('reinstallModpack: called', { instancePath: instance.value.path })
|
||||
debug('reinstallModpack: called', { instanceId: instance.value.id })
|
||||
reinstalling.value = true
|
||||
await update_repair_modrinth(instance.value.path).catch(handleError)
|
||||
reinstalling.value = false
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
})
|
||||
let shouldTrack = false
|
||||
try {
|
||||
if (isImportedModpack.value) {
|
||||
shouldTrack = await installLocalModpackFromPicker()
|
||||
} else {
|
||||
await update_repair_modrinth(instance.value.id).catch(handleError)
|
||||
shouldTrack = true
|
||||
}
|
||||
} finally {
|
||||
reinstalling.value = false
|
||||
}
|
||||
if (shouldTrack) {
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
})
|
||||
}
|
||||
debug('reinstallModpack: done')
|
||||
},
|
||||
|
||||
async swapModpack() {
|
||||
debug('swapModpack: called', { instanceId: instance.value.id })
|
||||
reinstalling.value = true
|
||||
try {
|
||||
const installed = await installLocalModpackFromPicker()
|
||||
if (installed) {
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
reinstalling.value = false
|
||||
}
|
||||
debug('swapModpack: done')
|
||||
},
|
||||
|
||||
async unlinkModpack() {
|
||||
debug('unlinkModpack: called', { instancePath: instance.value.path })
|
||||
await edit(instance.value.path, {
|
||||
linked_data: null as unknown as undefined,
|
||||
debug('unlinkModpack: called', { instanceId: instance.value.id })
|
||||
await edit(instance.value.id, {
|
||||
link: null as unknown as undefined,
|
||||
})
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['linkedModpackInfo', instance.value.path],
|
||||
queryKey: ['linkedModpackInfo', instance.value.id],
|
||||
})
|
||||
onUnlinked()
|
||||
debug('unlinkModpack: done')
|
||||
@@ -326,11 +439,9 @@ provideInstallationSettings({
|
||||
getCachedModpackVersions: () => null,
|
||||
async fetchModpackVersions() {
|
||||
debug('fetchModpackVersions: called', {
|
||||
projectId: instance.value.linked_data?.project_id,
|
||||
projectId: instance.value.link?.project_id,
|
||||
})
|
||||
const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch(
|
||||
handleError,
|
||||
)
|
||||
const versions = await get_project_versions(instance.value.link!.project_id!).catch(handleError)
|
||||
debug('fetchModpackVersions: done', { count: versions?.length ?? 0 })
|
||||
return (versions ?? []) as Labrinth.Versions.v2.Version[]
|
||||
},
|
||||
@@ -345,19 +456,18 @@ provideInstallationSettings({
|
||||
async onModpackVersionConfirm(version) {
|
||||
debug('onModpackVersionConfirm: called', {
|
||||
versionId: version.id,
|
||||
instancePath: instance.value.path,
|
||||
instanceId: instance.value.id,
|
||||
})
|
||||
await update_managed_modrinth_version(instance.value.path, version.id)
|
||||
await update_managed_modrinth_version(instance.value.id, version.id)
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['linkedModpackInfo', instance.value.path],
|
||||
queryKey: ['linkedModpackInfo', instance.value.id],
|
||||
})
|
||||
debug('onModpackVersionConfirm: done')
|
||||
},
|
||||
|
||||
updaterModalProps: computed(() => ({
|
||||
isApp: true,
|
||||
currentVersionId:
|
||||
modpackInfo.value?.update_version_id ?? instance.value.linked_data?.version_id ?? '',
|
||||
currentVersionId: modpackInfo.value?.update_version_id ?? instance.value.link?.version_id ?? '',
|
||||
projectIconUrl: modpackInfo.value?.project?.icon_url,
|
||||
projectName: modpackInfo.value?.project?.title ?? 'Modpack',
|
||||
currentGameVersion: instance.value.game_version,
|
||||
@@ -366,12 +476,39 @@ provideInstallationSettings({
|
||||
|
||||
isServer: false,
|
||||
isApp: true,
|
||||
showModpackVersionActions: !isMinecraftServer.value,
|
||||
showModpackVersionActions: computed(
|
||||
() =>
|
||||
isModrinthLinkedModpack.value &&
|
||||
!isMinecraftServer.value &&
|
||||
!isSharedInstanceManagedModpack.value,
|
||||
),
|
||||
isLocalFile: isImportedModpack,
|
||||
isManagedModpack: isSharedInstanceManagedModpack,
|
||||
managedModpackWarning: managedContentPolicy.managedModpackWarning,
|
||||
repairing,
|
||||
reinstalling,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<InstallationSettingsLayout />
|
||||
<InstallationSettingsLayout>
|
||||
<template #extra>
|
||||
<SharedInstanceInstallationSettingsControls
|
||||
:can-unpublish="canUnpublishSharedInstance"
|
||||
:can-unlink="canUnlinkSharedInstance"
|
||||
:busy="
|
||||
instance.install_stage !== 'installed' ||
|
||||
repairing ||
|
||||
reinstalling ||
|
||||
unpublishingSharedInstance ||
|
||||
unlinkingSharedInstance ||
|
||||
!!offline
|
||||
"
|
||||
:unpublishing="unpublishingSharedInstance"
|
||||
:unlinking="unlinkingSharedInstance"
|
||||
:unpublish="unpublishSharedInstance"
|
||||
:unlink="unlinkSharedInstance"
|
||||
/>
|
||||
</template>
|
||||
</InstallationSettingsLayout>
|
||||
</template>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { computed, readonly, ref, watch } from '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 { edit, get_optimal_jre_key } from '@/helpers/instance'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||
|
||||
@@ -36,7 +36,7 @@ const { instance } = injectInstanceSettings()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
|
||||
|
||||
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
|
||||
const optimalJava = readonly(await get_optimal_jre_key(instance.value.id).catch(handleError))
|
||||
|
||||
const overrideJavaInstall = ref(!!instance.value.java_path)
|
||||
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
|
||||
@@ -104,7 +104,7 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
|
||||
snapPoints: number[]
|
||||
}
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editInstanceObject = computed(() => {
|
||||
return {
|
||||
java_path:
|
||||
overrideJavaInstall.value && javaPath.value
|
||||
@@ -136,7 +136,7 @@ watch(
|
||||
memory,
|
||||
],
|
||||
async () => {
|
||||
await edit(instance.value.path, editProfileObject.value)
|
||||
await edit(instance.value.id, editInstanceObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { computed, type Ref, ref, watch } from 'vue'
|
||||
|
||||
import { edit } from '@/helpers/profile'
|
||||
import { edit } from '@/helpers/instance'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||
|
||||
@@ -32,7 +32,7 @@ const fullscreenSetting: Ref<boolean> = ref(
|
||||
instance.value.force_fullscreen ?? globalSettings.force_fullscreen,
|
||||
)
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editInstanceObject = computed(() => {
|
||||
if (!overrideWindowSettings.value) {
|
||||
return {
|
||||
force_fullscreen: null,
|
||||
@@ -48,7 +48,7 @@ const editProfileObject = computed(() => {
|
||||
watch(
|
||||
[overrideWindowSettings, resolution, fullscreenSetting],
|
||||
async () => {
|
||||
await edit(instance.value.path, editProfileObject.value)
|
||||
await edit(instance.value.id, editInstanceObject.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ defineProps<{
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||
size="24px"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
/>
|
||||
{{ instance.name }} <ChevronRightIcon />
|
||||
</span>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { PlatformTag } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
@@ -27,14 +27,15 @@ import InstallationSettings from '@/components/ui/instance_settings/Installation
|
||||
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
||||
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||
import { get_project_v3 } from '@/helpers/cache'
|
||||
import { get_linked_modpack_info } from '@/helpers/instance'
|
||||
import { get_loader_versions } from '@/helpers/metadata'
|
||||
import { get_linked_modpack_info } from '@/helpers/profile'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { provideInstanceSettings } from '@/providers/instance-settings'
|
||||
|
||||
import type { GameInstance } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
@@ -66,8 +67,8 @@ watch(
|
||||
() => props.instance,
|
||||
(instance) => {
|
||||
isMinecraftServer.value = false
|
||||
if (instance.linked_data?.project_id) {
|
||||
get_project_v3(instance.linked_data.project_id, 'must_revalidate')
|
||||
if (instance.link?.project_id) {
|
||||
get_project_v3(instance.link.project_id, 'must_revalidate')
|
||||
.then((project: Labrinth.Projects.v3.Project | undefined) => {
|
||||
if (project?.minecraft_server != null) {
|
||||
isMinecraftServer.value = true
|
||||
@@ -156,12 +157,18 @@ useQuery({
|
||||
queryFn: getSupportedModpackLoaders,
|
||||
})
|
||||
useQuery({
|
||||
queryKey: computed(() => ['linkedModpackInfo', props.instance.path]),
|
||||
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
|
||||
enabled: computed(() => !!props.instance.linked_data?.project_id && !props.offline),
|
||||
queryKey: computed(() => ['linkedModpackInfo', props.instance.id]),
|
||||
queryFn: () => get_linked_modpack_info(props.instance.id, 'stale_while_revalidate'),
|
||||
enabled: computed(() => !!props.instance.link?.project_id && !props.offline),
|
||||
})
|
||||
|
||||
function show(tabIndex?: number) {
|
||||
if (props.instance.link?.project_id) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['linkedModpackInfo', props.instance.id],
|
||||
queryFn: () => get_linked_modpack_info(props.instance.id, 'stale_while_revalidate'),
|
||||
})
|
||||
}
|
||||
tabbedModal.value?.show()
|
||||
if (tabIndex !== undefined) {
|
||||
nextTick(() => tabbedModal.value?.setTab(tabIndex))
|
||||
@@ -182,7 +189,7 @@ defineExpose({ show, hide })
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : undefined"
|
||||
size="24px"
|
||||
:tint-by="props.instance.path"
|
||||
:tint-by="props.instance.id"
|
||||
/>
|
||||
{{ instance.name }} <ChevronRightIcon />
|
||||
<span class="font-extrabold text-contrast">{{
|
||||
|
||||
@@ -68,17 +68,17 @@ const messages = defineMessages({
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'go-to-instance', instancePath: string): void
|
||||
(e: 'go-to-instance', instanceId: string): void
|
||||
(e: 'create-anyway'): void
|
||||
}>()
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const instanceName = ref('')
|
||||
const instancePath = ref('')
|
||||
const instanceId = ref('')
|
||||
|
||||
function show(name: string, path: string) {
|
||||
function show(name: string, id: string) {
|
||||
instanceName.value = name
|
||||
instancePath.value = path
|
||||
instanceId.value = id
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ function handleCancel() {
|
||||
|
||||
function handleGoToInstance() {
|
||||
modal.value?.hide()
|
||||
emit('go-to-instance', instancePath.value)
|
||||
emit('go-to-instance', instanceId.value)
|
||||
}
|
||||
|
||||
function handleCreateAnyway() {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:header="formatMessage(messages.header)"
|
||||
:on-hide="() => finish(false)"
|
||||
no-padding
|
||||
max-width="548px"
|
||||
width="100%"
|
||||
>
|
||||
<div class="flex w-full flex-col gap-6 p-6">
|
||||
<div class="flex flex-col gap-2 px-3">
|
||||
<h2 class="m-0 text-xl font-semibold leading-7 text-contrast">
|
||||
{{ formatMessage(messages.signInHeading) }}
|
||||
</h2>
|
||||
<p class="m-0 text-base leading-6 text-primary">
|
||||
{{ formatMessage(messages.description) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="w-full !shadow-none"
|
||||
type="button"
|
||||
:disabled="authenticating !== null"
|
||||
@click="authenticate('sign-up')"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="authenticating === 'sign-up'"
|
||||
aria-hidden="true"
|
||||
class="animate-spin"
|
||||
/>
|
||||
<UserPlusIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.createAccountButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="w-full"
|
||||
type="button"
|
||||
:disabled="authenticating !== null"
|
||||
@click="authenticate('sign-in')"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="authenticating === 'sign-in'"
|
||||
aria-hidden="true"
|
||||
class="animate-spin"
|
||||
/>
|
||||
<LogInIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(messages.signInButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-center text-base font-medium leading-6 text-primary">
|
||||
<IntlFormatted :message-id="messages.supportPrompt">
|
||||
<template #support="{ children }">
|
||||
<button
|
||||
type="button"
|
||||
class="inline cursor-pointer border-0 bg-transparent p-0 text-base font-medium leading-6 text-blue hover:underline"
|
||||
@click="openSupport"
|
||||
>
|
||||
<component :is="() => children" />
|
||||
</button>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LogInIcon, SpinnerIcon, UserPlusIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, IntlFormatted, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { ModrinthAuthFlow } from '@/helpers/mr_auth'
|
||||
|
||||
const props = defineProps<{
|
||||
requestAuth: (flow: ModrinthAuthFlow) => Promise<boolean>
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const authenticating = ref<ModrinthAuthFlow | null>(null)
|
||||
let resolveShow: ((signedIn: boolean) => void) | undefined
|
||||
|
||||
function show(event?: MouseEvent) {
|
||||
resolveShow?.(false)
|
||||
const modalInstance = modal.value
|
||||
if (!modalInstance) return Promise.resolve(false)
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolveShow = resolve
|
||||
modalInstance.show(event)
|
||||
})
|
||||
}
|
||||
|
||||
function finish(signedIn: boolean) {
|
||||
resolveShow?.(signedIn)
|
||||
resolveShow = undefined
|
||||
}
|
||||
|
||||
async function authenticate(flow: ModrinthAuthFlow) {
|
||||
authenticating.value = flow
|
||||
try {
|
||||
if (await props.requestAuth(flow)) {
|
||||
finish(true)
|
||||
modal.value?.hide()
|
||||
}
|
||||
} finally {
|
||||
authenticating.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function openSupport() {
|
||||
openUrl('https://support.modrinth.com')
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
header: {
|
||||
id: 'modal.modrinth-account-required.header',
|
||||
defaultMessage: 'Account required',
|
||||
},
|
||||
signInHeading: {
|
||||
id: 'modal.modrinth-account-required.sign-in-heading',
|
||||
defaultMessage: 'Sign in to a Modrinth account',
|
||||
},
|
||||
description: {
|
||||
id: 'modal.modrinth-account-required.description',
|
||||
defaultMessage:
|
||||
"You'll need to sign into your Modrinth account before you can use this feature.",
|
||||
},
|
||||
createAccountButton: {
|
||||
id: 'modal.modrinth-account-required.create-account-button',
|
||||
defaultMessage: 'Create an account',
|
||||
},
|
||||
signInButton: {
|
||||
id: 'modal.modrinth-account-required.sign-in-button',
|
||||
defaultMessage: 'Sign in to Modrinth',
|
||||
},
|
||||
supportPrompt: {
|
||||
id: 'modal.modrinth-account-required.support-prompt',
|
||||
defaultMessage: 'Having trouble signing in? <support>Get support</support>',
|
||||
},
|
||||
})
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
@@ -31,7 +31,8 @@ import dayjs from 'dayjs'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||
import { wait_for_install_job } from '@/helpers/install'
|
||||
import { update_managed_modrinth_version } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
|
||||
@@ -76,6 +77,11 @@ const { formatMessage } = useVIntl()
|
||||
const { startInstallingServer, stopInstallingServer } = injectServerInstall()
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
complete: []
|
||||
}>()
|
||||
|
||||
const diffModal = ref<InstanceType<typeof ContentDiffModal>>()
|
||||
const instance = ref<GameInstance | null>(null)
|
||||
const onUpdateComplete = ref<UpdateCompleteCallback>(() => {})
|
||||
@@ -83,15 +89,15 @@ const diffs = ref<DependencyDiff[]>([])
|
||||
const modpackVersionId = ref<string | null>(null)
|
||||
const modpackVersion = ref<Version | null>(null)
|
||||
|
||||
const normalizedDiffs = computed<ContentDiffItem[]>(() =>
|
||||
diffs.value.map((diff) => ({
|
||||
const normalizedDiffs = computed<ContentDiffItem[]>(() => {
|
||||
return diffs.value.map((diff) => ({
|
||||
type: diff.type,
|
||||
projectName: diff.project?.title,
|
||||
fileName: diff.fileName,
|
||||
currentVersionName: diff.currentVersion?.version_number,
|
||||
newVersionName: diff.newVersion?.version_number,
|
||||
})),
|
||||
)
|
||||
}))
|
||||
})
|
||||
|
||||
async function computeDependencyDiffs(
|
||||
currentDeps: Dependency[],
|
||||
@@ -202,15 +208,15 @@ async function computeDependencyDiffs(
|
||||
}
|
||||
|
||||
async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[] | null> {
|
||||
if (!inst.linked_data) return null
|
||||
if (!modpackVersionId.value || !inst.linked_data.version_id) return null
|
||||
if (!inst.link) return null
|
||||
if (!modpackVersionId.value || !inst.link.version_id) return null
|
||||
|
||||
try {
|
||||
// For server projects, linked_data.project_id is the server project but
|
||||
// linked_data.version_id references a content modpack version from a different project.
|
||||
// Detect this by comparing the version's project_id with linked_data.project_id.
|
||||
// For server projects, link.project_id is the server project but
|
||||
// link.version_id references a content modpack version from a different project.
|
||||
// Detect this by comparing the version's project_id with link.project_id.
|
||||
modpackVersion.value = await get_version(modpackVersionId.value, 'bypass')
|
||||
const instanceModpackVersion = await get_version(inst.linked_data.version_id, 'bypass')
|
||||
const instanceModpackVersion = await get_version(inst.link.version_id, 'bypass')
|
||||
|
||||
// Compute dependency diffs between current and latest version
|
||||
if (instanceModpackVersion && modpackVersion.value) {
|
||||
@@ -237,31 +243,30 @@ watch(
|
||||
)
|
||||
|
||||
async function handleUpdate() {
|
||||
hide()
|
||||
const serverProjectId = instance.value?.linked_data?.project_id
|
||||
const serverProjectId = instance.value?.link?.project_id
|
||||
if (serverProjectId) startInstallingServer(serverProjectId)
|
||||
try {
|
||||
if (modpackVersionId.value && instance.value) {
|
||||
await update_managed_modrinth_version(instance.value.path, modpackVersionId.value)
|
||||
const job = await update_managed_modrinth_version(instance.value.id, modpackVersionId.value)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await onUpdateComplete.value()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating instance:', error)
|
||||
} finally {
|
||||
if (serverProjectId) stopInstallingServer(serverProjectId)
|
||||
emit('complete')
|
||||
}
|
||||
}
|
||||
|
||||
function handleReport() {
|
||||
if (instance.value?.linked_data?.project_id) {
|
||||
openUrl(
|
||||
`https://modrinth.com/report?item=project&itemID=${instance.value.linked_data.project_id}`,
|
||||
)
|
||||
if (instance.value?.link?.project_id) {
|
||||
openUrl(`https://modrinth.com/report?item=project&itemID=${instance.value.link.project_id}`)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDecline() {
|
||||
hide()
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function show(
|
||||
@@ -292,16 +297,13 @@ const messages = defineMessages({
|
||||
updateRequiredDescription: {
|
||||
id: 'app.modal.update-to-play.update-required-description',
|
||||
defaultMessage:
|
||||
'An update is required to play {name}. Please update to the latest version to launch the game.',
|
||||
'An update is required to play {name}. Please update to latest version to launch the game.',
|
||||
},
|
||||
})
|
||||
|
||||
const hasUpdate = computed(() => {
|
||||
if (!instance.value?.linked_data) return false
|
||||
return (
|
||||
modpackVersionId.value != null &&
|
||||
modpackVersionId.value !== instance.value.linked_data.version_id
|
||||
)
|
||||
if (!instance.value?.link) return false
|
||||
return modpackVersionId.value != null && modpackVersionId.value !== instance.value.link.version_id
|
||||
})
|
||||
|
||||
defineExpose({ show, hide, hasUpdate })
|
||||
|
||||
@@ -11,6 +11,7 @@ const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
|
||||
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
||||
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
||||
|
||||
@@ -101,6 +102,15 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
|
||||
},
|
||||
skipNonEssentialWarningsTitle: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.title',
|
||||
defaultMessage: 'Skip non-essential warnings',
|
||||
},
|
||||
skipNonEssentialWarningsDescription: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.description',
|
||||
defaultMessage:
|
||||
'Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown.',
|
||||
},
|
||||
showPlayTimeTitle: {
|
||||
id: 'app.appearance-settings.show-play-time.title',
|
||||
defaultMessage: 'Show play time',
|
||||
@@ -285,6 +295,25 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
|
||||
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
|
||||
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
@@ -7,9 +7,13 @@ import { ref, watch } from 'vue'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const themeStore = useTheming()
|
||||
const settings = ref(await get())
|
||||
const purgeCacheConfirmModal = ref(null)
|
||||
|
||||
watch(
|
||||
settings,
|
||||
@@ -48,6 +52,19 @@ async function purgeCache() {
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
function handlePurgeCacheClick() {
|
||||
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
void purgeCache()
|
||||
return
|
||||
}
|
||||
|
||||
purgeCacheConfirmModal.value?.show()
|
||||
}
|
||||
|
||||
async function openDbBackupsFolder() {
|
||||
await showAppDbBackupsFolder().catch(handleError)
|
||||
}
|
||||
|
||||
async function findLauncherDir() {
|
||||
const newDir = await open({
|
||||
multiple: false,
|
||||
@@ -97,7 +114,7 @@ async function findLauncherDir() {
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
|
||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
@@ -136,5 +153,16 @@ async function findLauncherDir() {
|
||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">App database backups</h2>
|
||||
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
|
||||
<FolderOpenIcon />
|
||||
Open backups folder
|
||||
</button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
Backups of important app data are stored here in case you need to recover them later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div v-if="canUnpublish" class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">{{ formatMessage(messages.title) }}</span>
|
||||
<div>
|
||||
<ButtonStyled color="orange">
|
||||
<button :disabled="busy" @click="unpublishModal?.show()">
|
||||
<SpinnerIcon v-if="unpublishing" class="animate-spin" />
|
||||
<UnlinkIcon v-else class="size-5" />
|
||||
{{ formatMessage(unpublishing ? messages.unpublishingButton : messages.unpublishButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<span class="text-primary">{{ formatMessage(messages.unpublishDescription) }}</span>
|
||||
</div>
|
||||
<div v-if="canUnlink" class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.linkedTitle)
|
||||
}}</span>
|
||||
<div>
|
||||
<ButtonStyled color="orange">
|
||||
<button :disabled="busy" @click="unlinkModal?.show()">
|
||||
<SpinnerIcon v-if="unlinking" class="animate-spin" />
|
||||
<UnlinkIcon v-else class="size-5" />
|
||||
{{ formatMessage(unlinking ? messages.unlinkingButton : messages.unlinkButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<span class="text-primary">{{ formatMessage(messages.unlinkDescription) }}</span>
|
||||
</div>
|
||||
|
||||
<NewModal
|
||||
ref="unpublishModal"
|
||||
:header="formatMessage(messages.unpublishModalHeader)"
|
||||
fade="warning"
|
||||
max-width="500px"
|
||||
>
|
||||
<Admonition type="warning" :header="formatMessage(messages.unpublishModalAdmonitionHeader)">{{
|
||||
formatMessage(messages.unpublishModalBody)
|
||||
}}</Admonition>
|
||||
<template #actions
|
||||
><div class="flex justify-end gap-2">
|
||||
<ButtonStyled
|
||||
><button @click="unpublishModal?.hide()">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button></ButtonStyled
|
||||
><ButtonStyled color="orange"
|
||||
><button :disabled="busy" @click="confirmUnpublish">
|
||||
<UnlinkIcon />{{ formatMessage(messages.unpublishButton) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
</NewModal>
|
||||
<NewModal
|
||||
ref="unlinkModal"
|
||||
:header="formatMessage(messages.unlinkModalHeader)"
|
||||
fade="warning"
|
||||
max-width="500px"
|
||||
:on-hide="() => backupCreator?.cancelBackup()"
|
||||
>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition type="warning" :header="formatMessage(messages.unlinkModalAdmonitionHeader)">{{
|
||||
formatMessage(messages.unlinkModalBody)
|
||||
}}</Admonition>
|
||||
<InlineBackupCreator
|
||||
ref="backupCreator"
|
||||
backup-name="Before unlinking shared instance"
|
||||
@update:buttons-disabled="backupBusy = $event"
|
||||
/>
|
||||
</div>
|
||||
<template #actions
|
||||
><div class="flex justify-end gap-2">
|
||||
<ButtonStyled
|
||||
><button @click="unlinkModal?.hide()">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button></ButtonStyled
|
||||
><ButtonStyled color="orange"
|
||||
><button :disabled="busy || backupBusy" @click="confirmUnlink">
|
||||
<UnlinkIcon />{{ formatMessage(messages.unlinkButton) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
</div></template
|
||||
>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SpinnerIcon, UnlinkIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
InlineBackupCreator,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
canUnpublish: boolean
|
||||
canUnlink: boolean
|
||||
busy: boolean
|
||||
unpublishing: boolean
|
||||
unlinking: boolean
|
||||
unpublish: () => Promise<void>
|
||||
unlink: () => Promise<void>
|
||||
}>()
|
||||
const { formatMessage } = useVIntl()
|
||||
const unpublishModal = ref<InstanceType<typeof NewModal>>()
|
||||
const unlinkModal = ref<InstanceType<typeof NewModal>>()
|
||||
const backupCreator = ref<InstanceType<typeof InlineBackupCreator>>()
|
||||
const backupBusy = ref(false)
|
||||
|
||||
async function confirmUnpublish() {
|
||||
unpublishModal.value?.hide()
|
||||
await props.unpublish()
|
||||
}
|
||||
async function confirmUnlink() {
|
||||
unlinkModal.value?.hide()
|
||||
await props.unlink()
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'installation-settings.shared-instance.title', defaultMessage: 'Shared instance' },
|
||||
linkedTitle: {
|
||||
id: 'installation-settings.shared-instance.linked-title',
|
||||
defaultMessage: 'Linked shared instance',
|
||||
},
|
||||
unpublishButton: {
|
||||
id: 'installation-settings.shared-instance.unpublish-button',
|
||||
defaultMessage: 'Unpublish shared instance',
|
||||
},
|
||||
unpublishingButton: {
|
||||
id: 'installation-settings.shared-instance.unpublishing-button',
|
||||
defaultMessage: 'Unpublishing...',
|
||||
},
|
||||
unpublishDescription: {
|
||||
id: 'installation-settings.shared-instance.unpublish-description',
|
||||
defaultMessage: 'Stop sharing this instance and remove it from Modrinth.',
|
||||
},
|
||||
unlinkButton: {
|
||||
id: 'installation-settings.shared-instance.unlink-button',
|
||||
defaultMessage: 'Unlink shared instance',
|
||||
},
|
||||
unlinkingButton: {
|
||||
id: 'installation-settings.shared-instance.unlinking-button',
|
||||
defaultMessage: 'Unlinking...',
|
||||
},
|
||||
unlinkDescription: {
|
||||
id: 'installation-settings.shared-instance.unlink-description',
|
||||
defaultMessage: 'Disconnect this local instance from future shared updates.',
|
||||
},
|
||||
unpublishModalHeader: {
|
||||
id: 'installation-settings.unpublish-shared-instance.modal.header',
|
||||
defaultMessage: 'Unpublish shared instance',
|
||||
},
|
||||
unpublishModalAdmonitionHeader: {
|
||||
id: 'installation-settings.unpublish-shared-instance.modal.admonition-header',
|
||||
defaultMessage: 'Unpublishing shared instance',
|
||||
},
|
||||
unpublishModalBody: {
|
||||
id: 'installation-settings.unpublish-shared-instance.modal.admonition-body',
|
||||
defaultMessage:
|
||||
"This deletes the shared instance from Modrinth's servers. People using it in the Modrinth App will stop receiving updates, but your local instance and its content will stay on this device.",
|
||||
},
|
||||
unlinkModalHeader: {
|
||||
id: 'installation-settings.unlink-shared-instance.modal.header',
|
||||
defaultMessage: 'Unlink shared instance',
|
||||
},
|
||||
unlinkModalAdmonitionHeader: {
|
||||
id: 'installation-settings.unlink-shared-instance.modal.admonition-header',
|
||||
defaultMessage: 'Unlinking shared instance',
|
||||
},
|
||||
unlinkModalBody: {
|
||||
id: 'installation-settings.unlink-shared-instance.modal.admonition-body',
|
||||
defaultMessage:
|
||||
'This only affects your local instance. Your installed content will stay on this device, and the shared instance and other people using it will not be affected.',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<ContentDiffModal
|
||||
ref="modal"
|
||||
:header="formatMessage(messages.updateToPlay)"
|
||||
:admonition-header="formatMessage(messages.updateRequired)"
|
||||
:description="instance ? formatMessage(messages.description, { name: instance.name }) : ''"
|
||||
:diffs="diffs"
|
||||
:confirm-label="formatMessage(commonMessages.updateButton)"
|
||||
:confirm-icon="DownloadIcon"
|
||||
:added-label="formatMessage(messages.addedLabel)"
|
||||
:removed-label="formatMessage(messages.removedLabel)"
|
||||
:show-report-button="false"
|
||||
@confirm="update"
|
||||
@cancel="emit('cancel')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon } from '@modrinth/assets'
|
||||
import {
|
||||
commonMessages,
|
||||
type ContentDiffItem,
|
||||
ContentDiffModal,
|
||||
defineMessages,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
getSharedInstanceUnavailableReason,
|
||||
install_update_shared_instance,
|
||||
isSharedInstanceUnavailableError,
|
||||
type SharedInstanceUnavailableReason,
|
||||
type SharedInstanceUpdatePreview,
|
||||
wait_for_install_job,
|
||||
} from '@/helpers/install'
|
||||
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
const emit = defineEmits<{
|
||||
cancel: []
|
||||
complete: []
|
||||
sharedInstanceUnavailable: [reason: SharedInstanceUnavailableReason | null]
|
||||
}>()
|
||||
const modal = ref<InstanceType<typeof ContentDiffModal>>()
|
||||
const instance = ref<GameInstance | null>(null)
|
||||
const preview = ref<SharedInstanceUpdatePreview | null>(null)
|
||||
const onComplete = ref<UpdateCompleteCallback>(() => {})
|
||||
const { formatMessage } = useVIntl()
|
||||
const { notifySharedInstanceError } = useSharedInstanceErrors()
|
||||
const diffs = computed<ContentDiffItem[]>(
|
||||
() =>
|
||||
preview.value?.diffs.map((diff) => ({
|
||||
type: diff.type,
|
||||
projectName: diff.projectName ?? undefined,
|
||||
fileName: diff.fileName ?? undefined,
|
||||
currentVersionName: diff.currentVersionName ?? undefined,
|
||||
newVersionName: diff.newVersionName ?? undefined,
|
||||
disabled: diff.disabled,
|
||||
})) ?? [],
|
||||
)
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
if (instance.value) {
|
||||
const job = await install_update_shared_instance(instance.value.id)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await onComplete.value()
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSharedInstanceUnavailableError(error)) {
|
||||
emit('sharedInstanceUnavailable', getSharedInstanceUnavailableReason(error))
|
||||
return
|
||||
}
|
||||
notifySharedInstanceError(error)
|
||||
} finally {
|
||||
emit('complete')
|
||||
}
|
||||
}
|
||||
|
||||
function show(
|
||||
instanceValue: GameInstance,
|
||||
previewValue: SharedInstanceUpdatePreview,
|
||||
callback: UpdateCompleteCallback = () => {},
|
||||
event?: MouseEvent,
|
||||
) {
|
||||
instance.value = instanceValue
|
||||
preview.value = previewValue
|
||||
onComplete.value = callback
|
||||
modal.value?.show(event)
|
||||
}
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
updateToPlay: { id: 'app.modal.update-to-play.header', defaultMessage: 'Update to play' },
|
||||
updateRequired: {
|
||||
id: 'app.modal.update-to-play.update-required',
|
||||
defaultMessage: 'Update required',
|
||||
},
|
||||
description: {
|
||||
id: 'app.modal.update-to-play.update-required-description',
|
||||
defaultMessage:
|
||||
'An update is required to play {name}. Please update to latest version to launch the game.',
|
||||
},
|
||||
addedLabel: {
|
||||
id: 'app.modal.update-to-play.shared-instance-added-label',
|
||||
defaultMessage: 'Added',
|
||||
},
|
||||
removedLabel: {
|
||||
id: 'app.modal.update-to-play.shared-instance-removed-label',
|
||||
defaultMessage: 'Removed',
|
||||
},
|
||||
})
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<NewModal ref="modal" :header="formatMessage(messages.installToPlay)" :closable="true" no-padding>
|
||||
<div v-if="preview" class="flex flex-col gap-6 max-w-[500px] p-6 pb-2">
|
||||
<Admonition type="warning" :header="formatMessage(messages.trustWarningHeader)">
|
||||
{{ formatMessage(messages.trustWarningDescription) }}
|
||||
</Admonition>
|
||||
<SharedInstanceInstallSummary :preview="preview" @view-contents="openViewContents" />
|
||||
<div v-if="preview.externalFileCount" class="flex items-center gap-2 text-orange">
|
||||
<IssuesIcon class="h-5 w-5 flex-none" />
|
||||
<span>{{
|
||||
formatMessage(messages.externalFileWarning, { count: preview.externalFileCount })
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled
|
||||
><button @click="hide">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
<ButtonStyled color="brand"
|
||||
><button @click="accept">
|
||||
<DownloadIcon />{{ formatMessage(messages.installButton) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
<ModpackContentModal
|
||||
ref="contentModal"
|
||||
:header="formatMessage(messages.sharedInstanceContent)"
|
||||
:modpack-name="preview?.name ?? ''"
|
||||
:modpack-icon-url="preview?.iconUrl ?? undefined"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, IssuesIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
ModpackContentModal,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
|
||||
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
|
||||
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
|
||||
const preview = ref<SharedInstanceInstallPreview | null>(null)
|
||||
const install = ref<() => void | Promise<void>>(() => {})
|
||||
const { formatMessage } = useVIntl()
|
||||
const { load } = useSharedInstancePreviewContent()
|
||||
|
||||
async function accept() {
|
||||
hide()
|
||||
try {
|
||||
await install.value()
|
||||
} catch (error) {
|
||||
console.error('Failed to install shared instance:', error)
|
||||
}
|
||||
}
|
||||
async function openViewContents() {
|
||||
if (!preview.value) return
|
||||
contentModal.value?.showLoading()
|
||||
try {
|
||||
contentModal.value?.show(await load(preview.value))
|
||||
} catch (error) {
|
||||
console.error('Failed to load shared instance contents:', error)
|
||||
contentModal.value?.show([])
|
||||
}
|
||||
}
|
||||
function show(
|
||||
previewValue: SharedInstanceInstallPreview,
|
||||
installValue: () => void | Promise<void>,
|
||||
event?: MouseEvent,
|
||||
) {
|
||||
preview.value = previewValue
|
||||
install.value = installValue
|
||||
hide_ads_window()
|
||||
modal.value?.show(event)
|
||||
}
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
installToPlay: { id: 'app.modal.install-to-play.header', defaultMessage: 'Install to play' },
|
||||
sharedInstanceContent: {
|
||||
id: 'app.modal.install-to-play.shared-instance-content',
|
||||
defaultMessage: 'Shared instance content',
|
||||
},
|
||||
trustWarningHeader: {
|
||||
id: 'app.modal.install-to-play.trust-warning-header',
|
||||
defaultMessage: 'Do you trust this user?',
|
||||
},
|
||||
trustWarningDescription: {
|
||||
id: 'app.modal.install-to-play.trust-warning-description',
|
||||
defaultMessage:
|
||||
'A shared instance will install files on your computer and may include content not from Modrinth.',
|
||||
},
|
||||
externalFileWarning: {
|
||||
id: 'app.modal.install-to-play.external-file-warning',
|
||||
defaultMessage:
|
||||
'{count, plural, one {This instance includes # file that is not from Modrinth.} other {This instance includes # files that are not from Modrinth.}}',
|
||||
},
|
||||
installButton: { id: 'app.modal.install-to-play.install-button', defaultMessage: 'Install' },
|
||||
})
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold text-contrast">{{ formatMessage(messages.sharedInstance) }}</span>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="emit('viewContents')">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 rounded-xl bg-surface-2 p-3">
|
||||
<Avatar :src="preview.iconUrl" :alt="preview.name" size="48px" />
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-semibold text-contrast">{{ preview.name }}</span>
|
||||
<span class="text-sm text-secondary">
|
||||
{{ loaderDisplay }} {{ preview.gameVersion }}
|
||||
<template v-if="preview.modCount">
|
||||
<BulletDivider />
|
||||
{{ formatMessage(messages.modCount, { count: preview.modCount }) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EyeIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
BulletDivider,
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
formatLoader,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
|
||||
const props = defineProps<{ preview: SharedInstanceInstallPreview }>()
|
||||
const emit = defineEmits<{ viewContents: [] }>()
|
||||
const { formatMessage } = useVIntl()
|
||||
const loaderDisplay = computed(() =>
|
||||
props.preview.loader ? formatLoader(formatMessage, props.preview.loader) : '',
|
||||
)
|
||||
const messages = defineMessages({
|
||||
sharedInstance: {
|
||||
id: 'app.modal.install-to-play.shared-instance',
|
||||
defaultMessage: 'Shared instance',
|
||||
},
|
||||
viewContents: { id: 'app.modal.install-to-play.view-contents', defaultMessage: 'View contents' },
|
||||
modCount: {
|
||||
id: 'app.modal.install-to-play.mod-count',
|
||||
defaultMessage: '{count, plural, one {# mod} other {# mods}}',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { ContentItem } from '@modrinth/ui'
|
||||
|
||||
import { get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
|
||||
type VersionDependency = Labrinth.Versions.v2.Dependency & { version_id?: string }
|
||||
|
||||
export function useSharedInstancePreviewContent() {
|
||||
async function load(preview: SharedInstanceInstallPreview): Promise<ContentItem[]> {
|
||||
return [
|
||||
...preview.externalFiles.map(externalFileContentItem),
|
||||
...(await modpackContentItems(preview)),
|
||||
...(await contentItemsFromVersionIds(
|
||||
preview.contentVersionIds.filter((id) => id !== preview.modpackVersionId),
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
async function modpackContentItems(preview: SharedInstanceInstallPreview) {
|
||||
if (!preview.modpackVersionId) return []
|
||||
const version = await get_version(preview.modpackVersionId, 'must_revalidate')
|
||||
return await contentItemsFromDependencies(version?.dependencies ?? [])
|
||||
}
|
||||
|
||||
async function contentItemsFromDependencies(dependencies: Labrinth.Versions.v2.Dependency[]) {
|
||||
const deps = dependencies as VersionDependency[]
|
||||
const projectIds = unique(deps.map((dep) => dep.project_id).filter((id): id is string => !!id))
|
||||
const versionIds = unique(deps.map((dep) => dep.version_id).filter((id): id is string => !!id))
|
||||
const [projects, versions]: [Labrinth.Projects.v2.Project[], Labrinth.Versions.v2.Version[]] =
|
||||
await Promise.all([
|
||||
projectIds.length ? get_project_many(projectIds, 'must_revalidate') : [],
|
||||
versionIds.length ? get_version_many(versionIds, 'must_revalidate') : [],
|
||||
])
|
||||
const projectMap = new Map(projects.map((project) => [project.id, project]))
|
||||
const versionMap = new Map(versions.map((version) => [version.id, version]))
|
||||
|
||||
return deps.map((dependency): ContentItem => {
|
||||
const project = dependency.project_id ? projectMap.get(dependency.project_id) : null
|
||||
const version = dependency.version_id ? versionMap.get(dependency.version_id) : null
|
||||
const fileName =
|
||||
version?.files?.[0]?.filename ?? dependency.file_name ?? project?.title ?? 'Unknown'
|
||||
return contentItem(
|
||||
version?.id ?? project?.id ?? fileName,
|
||||
fileName,
|
||||
project,
|
||||
version,
|
||||
!project && !version,
|
||||
dependency.project_id ?? fileName,
|
||||
dependency.file_name ?? fileName,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async function contentItemsFromVersionIds(versionIds: string[]) {
|
||||
const versions: Labrinth.Versions.v2.Version[] = versionIds.length
|
||||
? await get_version_many(unique(versionIds), 'must_revalidate')
|
||||
: []
|
||||
const projectIds = unique(versions.map((version) => version.project_id).filter(Boolean))
|
||||
const projects: Labrinth.Projects.v2.Project[] = projectIds.length
|
||||
? await get_project_many(projectIds, 'must_revalidate')
|
||||
: []
|
||||
const projectMap = new Map(projects.map((project) => [project.id, project]))
|
||||
return versions.map((version): ContentItem => {
|
||||
const project = projectMap.get(version.project_id)
|
||||
const fileName = version.files?.[0]?.filename ?? project?.title ?? version.name ?? 'Unknown'
|
||||
return contentItem(
|
||||
version.id,
|
||||
fileName,
|
||||
project,
|
||||
version,
|
||||
false,
|
||||
version.project_id,
|
||||
version.name,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return { load }
|
||||
}
|
||||
|
||||
function contentItem(
|
||||
id: string,
|
||||
fileName: string,
|
||||
project?: Labrinth.Projects.v2.Project | null,
|
||||
version?: Labrinth.Versions.v2.Version | null,
|
||||
external = false,
|
||||
fallbackProjectId = id,
|
||||
fallbackTitle = fileName,
|
||||
projectType = project?.project_type ?? 'mod',
|
||||
): ContentItem {
|
||||
return {
|
||||
id,
|
||||
file_name: fileName,
|
||||
project_type: projectType,
|
||||
has_update: false,
|
||||
update_version_id: null,
|
||||
external,
|
||||
project: {
|
||||
id: project?.id ?? fallbackProjectId,
|
||||
slug: project?.slug ?? fallbackProjectId,
|
||||
title: project?.title ?? fallbackTitle,
|
||||
icon_url: project?.icon_url ?? undefined,
|
||||
},
|
||||
...(version
|
||||
? {
|
||||
version: {
|
||||
id: version.id,
|
||||
file_name: fileName,
|
||||
version_number: version.version_number ?? undefined,
|
||||
date_published: version.date_published ?? undefined,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
function externalFileContentItem(
|
||||
file: SharedInstanceInstallPreview['externalFiles'][number],
|
||||
): ContentItem {
|
||||
return contentItem(
|
||||
`external:${file.fileType}:${file.fileName}`,
|
||||
file.fileName,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
file.fileName,
|
||||
file.fileName,
|
||||
file.fileType,
|
||||
)
|
||||
}
|
||||
|
||||
function unique<T>(values: T[]) {
|
||||
return Array.from(new Set(values))
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<SharedInstanceInviteLinkModal ref="inviteLinkModal" :process-invite="installFromInviteId" />
|
||||
<ModrinthAccountRequiredModal ref="accountRequiredModal" :request-auth="requestAuth" />
|
||||
<SharedInstanceInstallModal ref="installModal" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { injectAuth } from '@modrinth/ui'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import ModrinthAccountRequiredModal from '@/components/ui/modal/ModrinthAccountRequiredModal.vue'
|
||||
import SharedInstanceInstallModal from '@/components/ui/shared-instances/shared-instance-install-modal/index.vue'
|
||||
import type { ModrinthAuthFlow } from '@/helpers/mr_auth'
|
||||
|
||||
import SharedInstanceInviteLinkModal from './shared-instance-invite-link-modal.vue'
|
||||
import type { SharedInstanceInviteHandler } from './shared-instance-invite-types'
|
||||
import { useSharedInstanceInviteHandler } from './use-shared-instance-invite-handler'
|
||||
|
||||
const auth = injectAuth()
|
||||
const inviteLinkModal = ref<InstanceType<typeof SharedInstanceInviteLinkModal>>()
|
||||
const installModal = ref<InstanceType<typeof SharedInstanceInstallModal>>()
|
||||
const accountRequiredModal = ref<InstanceType<typeof ModrinthAccountRequiredModal>>()
|
||||
const { handleNotification, installFromInviteId } = useSharedInstanceInviteHandler(
|
||||
installModal,
|
||||
accountRequiredModal,
|
||||
)
|
||||
|
||||
async function requestAuth(flow: ModrinthAuthFlow) {
|
||||
await auth.requestSignIn('', flow)
|
||||
await nextTick()
|
||||
return !!auth.session_token.value
|
||||
}
|
||||
|
||||
function showManualInviteLinkModal(event?: MouseEvent) {
|
||||
inviteLinkModal.value?.show(event)
|
||||
}
|
||||
|
||||
defineExpose<SharedInstanceInviteHandler>({
|
||||
handleNotification,
|
||||
installFromInviteId,
|
||||
showManualInviteLinkModal,
|
||||
})
|
||||
</script>
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Install from invite link" max-width="32rem">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="font-semibold text-contrast">Invite link</span>
|
||||
<StyledInput
|
||||
v-model="inviteLink"
|
||||
placeholder="https://modrinth.com/share/..."
|
||||
:disabled="processing"
|
||||
@keydown.enter="process"
|
||||
/>
|
||||
<span v-if="inviteLink && !inviteId" class="text-sm text-red">
|
||||
Enter a valid Modrinth shared-instance invite link.
|
||||
</span>
|
||||
</div>
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled>
|
||||
<button :disabled="processing" @click="modal?.hide()"><XIcon /> Cancel</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!inviteId || processing" @click="process">
|
||||
<LinkIcon />
|
||||
Process
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LinkIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal, StyledInput } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { parseSharedInstanceInviteLink } from './shared-instance-invite-parser'
|
||||
|
||||
const props = defineProps<{ processInvite: (inviteId: string) => Promise<void> }>()
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const inviteLink = ref('')
|
||||
const processing = ref(false)
|
||||
const inviteId = computed(() => parseSharedInstanceInviteLink(inviteLink.value))
|
||||
|
||||
function show(event?: MouseEvent) {
|
||||
inviteLink.value = ''
|
||||
modal.value?.show(event)
|
||||
}
|
||||
|
||||
async function process() {
|
||||
if (!inviteId.value) return
|
||||
|
||||
processing.value = true
|
||||
modal.value?.hide()
|
||||
try {
|
||||
await props.processInvite(inviteId.value)
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import type {
|
||||
AppNotification,
|
||||
SharedInstanceInvite,
|
||||
SharedInstanceInviteNotificationBody,
|
||||
} from './shared-instance-invite-types'
|
||||
|
||||
function optionalString(value: unknown) {
|
||||
return typeof value === 'string' ? value : null
|
||||
}
|
||||
|
||||
export function parseSharedInstanceInviteNotification(
|
||||
notification: AppNotification,
|
||||
): SharedInstanceInvite | null {
|
||||
if (notification.body?.type !== 'shared_instance_invite') return null
|
||||
|
||||
const body = notification.body as SharedInstanceInviteNotificationBody
|
||||
const sharedInstanceId = optionalString(body.shared_instance_id)
|
||||
const sharedInstanceName = optionalString(body.shared_instance_name)
|
||||
if (!sharedInstanceId || !sharedInstanceName) return null
|
||||
|
||||
return {
|
||||
sharedInstanceId,
|
||||
sharedInstanceName,
|
||||
invitedById: optionalString(body.invited_by),
|
||||
invitedByUsername: optionalString(body.invited_by_username),
|
||||
invitedByAvatarUrl: optionalString(body.invited_by_avatar_url),
|
||||
instanceIconUrl: optionalString(body.instance_icon_url),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSharedInstanceInviteLink(value: string) {
|
||||
const trimmedValue = value.trim()
|
||||
if (!trimmedValue) return null
|
||||
|
||||
try {
|
||||
const url = new URL(trimmedValue)
|
||||
const match = /^\/share\/([^/]+)\/?$/.exec(url.pathname)
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
export type SharedInstanceInviteNotificationBody = {
|
||||
type: 'shared_instance_invite'
|
||||
shared_instance_id?: unknown
|
||||
shared_instance_name?: unknown
|
||||
invited_by?: unknown
|
||||
invited_by_username?: unknown
|
||||
invited_by_avatar_url?: unknown
|
||||
instance_icon_url?: unknown
|
||||
}
|
||||
|
||||
export type AppNotification = {
|
||||
id: string | number
|
||||
read?: boolean
|
||||
body?: { type?: unknown } & Record<string, unknown>
|
||||
}
|
||||
|
||||
export type SharedInstanceInvite = {
|
||||
sharedInstanceId: string
|
||||
sharedInstanceName: string
|
||||
invitedById: string | null
|
||||
invitedByUsername: string | null
|
||||
invitedByAvatarUrl: string | null
|
||||
instanceIconUrl: string | null
|
||||
}
|
||||
|
||||
export type SharedInstanceInviteHandler = {
|
||||
handleNotification(notification: AppNotification): Promise<boolean>
|
||||
installFromInviteId(inviteId: string): Promise<void>
|
||||
showManualInviteLinkModal(event?: MouseEvent): void
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import {
|
||||
injectAuth,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
injectPopupNotificationManager,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { type Ref, watch } from 'vue'
|
||||
|
||||
import { config } from '@/config'
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import {
|
||||
install_accept_shared_instance_invite,
|
||||
install_get_shared_instance_preview,
|
||||
install_shared_instance,
|
||||
} from '@/helpers/install'
|
||||
|
||||
import { parseSharedInstanceInviteNotification } from './shared-instance-invite-parser'
|
||||
import type { AppNotification, SharedInstanceInvite } from './shared-instance-invite-types'
|
||||
|
||||
type InstallModal = {
|
||||
show(
|
||||
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
|
||||
install: () => Promise<void>,
|
||||
): void
|
||||
}
|
||||
|
||||
type AccountRequiredModal = {
|
||||
show(event?: MouseEvent): Promise<boolean>
|
||||
}
|
||||
|
||||
export function useSharedInstanceInviteHandler(
|
||||
installModal: Ref<InstallModal | undefined>,
|
||||
accountRequiredModal: Ref<AccountRequiredModal | undefined>,
|
||||
) {
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { addPopupNotification } = injectPopupNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
const displayedNotifications = new Set<string | number>()
|
||||
|
||||
async function markNotificationRead(notification: AppNotification) {
|
||||
await client.labrinth.notifications_v2.markAsRead(String(notification.id))
|
||||
}
|
||||
|
||||
async function resolveInvite(invite: SharedInstanceInvite) {
|
||||
const invitedBy =
|
||||
!invite.invitedByUsername && invite.invitedById
|
||||
? await get_user(invite.invitedById, 'bypass').catch(() => null)
|
||||
: null
|
||||
|
||||
return {
|
||||
...invite,
|
||||
invitedByUsername: invite.invitedByUsername ?? invitedBy?.username ?? null,
|
||||
invitedByAvatarUrl: invite.invitedByAvatarUrl ?? invitedBy?.avatar_url ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function showInstall(
|
||||
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
|
||||
install: () => Promise<void>,
|
||||
) {
|
||||
if (!installModal.value) throw new Error('Shared instance install modal is not available.')
|
||||
installModal.value.show(preview, install)
|
||||
}
|
||||
|
||||
async function acceptNotification(notification: AppNotification, invite: SharedInstanceInvite) {
|
||||
try {
|
||||
const preview = await install_get_shared_instance_preview(
|
||||
invite.sharedInstanceId,
|
||||
invite.sharedInstanceName,
|
||||
)
|
||||
if (invite.instanceIconUrl) preview.iconUrl = invite.instanceIconUrl
|
||||
|
||||
showInstall(preview, async () => {
|
||||
await install_shared_instance(
|
||||
invite.sharedInstanceId,
|
||||
invite.sharedInstanceName,
|
||||
invite.invitedById,
|
||||
null,
|
||||
null,
|
||||
invite.instanceIconUrl,
|
||||
)
|
||||
await markNotificationRead(notification)
|
||||
await queryClient.invalidateQueries({ queryKey: ['instances'] })
|
||||
})
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNotification(notification: AppNotification) {
|
||||
const parsedInvite = parseSharedInstanceInviteNotification(notification)
|
||||
if (!parsedInvite) return false
|
||||
if (displayedNotifications.has(notification.id)) return true
|
||||
|
||||
displayedNotifications.add(notification.id)
|
||||
const invite = await resolveInvite(parsedInvite)
|
||||
addPopupNotification({
|
||||
title: invite.sharedInstanceName,
|
||||
autoCloseMs: null,
|
||||
toast: {
|
||||
type: 'instance-invite',
|
||||
actorName: invite.invitedByUsername,
|
||||
actorAvatarUrl: invite.invitedByAvatarUrl ?? undefined,
|
||||
entityName: invite.sharedInstanceName,
|
||||
entityIconUrl: invite.instanceIconUrl ?? undefined,
|
||||
onAccept: () => acceptNotification(notification, invite),
|
||||
onDecline: () =>
|
||||
markNotificationRead(notification).catch((error) => handleError(toError(error))),
|
||||
onOpenActor: () => {
|
||||
if (invite.invitedByUsername) {
|
||||
openUrl(`${config.siteUrl}/user/${encodeURIComponent(invite.invitedByUsername)}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
async function requireAccount() {
|
||||
if (!auth.isReady?.value) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const stop = watch(auth.isReady!, (ready) => {
|
||||
if (ready) {
|
||||
stop()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
if (auth.session_token.value) return true
|
||||
return (await accountRequiredModal.value?.show()) ?? false
|
||||
}
|
||||
|
||||
async function installFromInviteId(inviteId: string) {
|
||||
try {
|
||||
if (!(await requireAccount())) return
|
||||
const invite = await install_accept_shared_instance_invite(inviteId)
|
||||
showInstall(invite.preview, async () => {
|
||||
await install_shared_instance(
|
||||
invite.sharedInstanceId,
|
||||
invite.preview.name,
|
||||
invite.managerId,
|
||||
invite.serverManagerName,
|
||||
invite.serverManagerIconUrl,
|
||||
invite.instanceIconUrl,
|
||||
)
|
||||
await queryClient.invalidateQueries({ queryKey: ['instances'] })
|
||||
})
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
}
|
||||
}
|
||||
|
||||
return { handleNotification, installFromInviteId }
|
||||
}
|
||||
@@ -27,10 +27,10 @@ import { useRouter } from 'vue-router'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project } from '@/helpers/cache'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { showProfileInFolder } from '@/helpers/utils'
|
||||
import { showInstanceInFolder } from '@/helpers/utils'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
@@ -52,13 +52,13 @@ const props = defineProps<{
|
||||
last_played: Dayjs
|
||||
}>()
|
||||
|
||||
const loadingModpack = ref(!!props.instance.linked_data)
|
||||
const loadingModpack = ref(!!props.instance.link)
|
||||
|
||||
const modpack = ref()
|
||||
|
||||
if (props.instance.linked_data) {
|
||||
if (props.instance.link) {
|
||||
nextTick().then(async () => {
|
||||
modpack.value = await get_project(props.instance.linked_data?.project_id, 'must_revalidate')
|
||||
modpack.value = await get_project(props.instance.link?.project_id, 'must_revalidate')
|
||||
loadingModpack.value = false
|
||||
})
|
||||
}
|
||||
@@ -81,8 +81,8 @@ const playing = ref(false)
|
||||
const play = async (event: MouseEvent) => {
|
||||
event?.stopPropagation()
|
||||
loading.value = true
|
||||
await run(props.instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||
await run(props.instance.id)
|
||||
.catch((err) => handleSevereError(err, { instanceId: props.instance.id }))
|
||||
.finally(() => {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: props.instance.loader,
|
||||
@@ -97,7 +97,7 @@ const play = async (event: MouseEvent) => {
|
||||
const stop = async (event: MouseEvent) => {
|
||||
event?.stopPropagation()
|
||||
loading.value = true
|
||||
await kill(props.instance.path).catch(handleError)
|
||||
await kill(props.instance.id).catch(handleError)
|
||||
trackEvent('InstanceStop', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
@@ -112,7 +112,7 @@ const unlistenProcesses = await process_listener(async () => {
|
||||
})
|
||||
|
||||
const checkProcess = async () => {
|
||||
const runningProcesses = await get_by_profile_path(props.instance.path).catch(handleError)
|
||||
const runningProcesses = await get_by_instance_id(props.instance.id).catch(handleError)
|
||||
|
||||
playing.value = runningProcesses.length > 0
|
||||
}
|
||||
@@ -130,7 +130,7 @@ onUnmounted(() => {
|
||||
<template #clickable>
|
||||
<router-link
|
||||
class="no-click-animation"
|
||||
:to="`/instance/${encodeURIComponent(instance.path)}`"
|
||||
:to="`/instance/${encodeURIComponent(instance.id)}`"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
@@ -138,7 +138,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<Avatar
|
||||
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
||||
:tint-by="instance.path"
|
||||
:tint-by="instance.id"
|
||||
size="48px"
|
||||
/>
|
||||
<div class="flex flex-col col-span-2 justify-between h-full">
|
||||
@@ -206,12 +206,12 @@ onUnmounted(() => {
|
||||
:options="[
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instance.path,
|
||||
action: () => router.push(encodeURI(`/instance/${instance.path}`)),
|
||||
shown: !!instance.id,
|
||||
action: () => router.push(encodeURI(`/instance/${instance.id}`)),
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => showProfileInFolder(instance.path),
|
||||
action: () => showInstanceInFolder(instance.id),
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -10,13 +10,13 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { instance_listener, process_listener } from '@/helpers/events'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_all } from '@/helpers/process'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import {
|
||||
get_profile_protocol_version,
|
||||
get_instance_protocol_version,
|
||||
get_recent_worlds,
|
||||
getWorldIdentifier,
|
||||
hasServerQuickPlaySupport,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
type ServerWorld,
|
||||
start_join_server,
|
||||
start_join_singleplayer_world,
|
||||
type WorldWithProfile,
|
||||
type WorldWithInstance,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
@@ -66,7 +66,7 @@ type InstanceJumpBackInItem = BaseJumpBackInItem & {
|
||||
|
||||
type WorldJumpBackInItem = BaseJumpBackInItem & {
|
||||
type: 'world'
|
||||
world: WorldWithProfile
|
||||
world: WorldWithInstance
|
||||
}
|
||||
|
||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
||||
@@ -100,7 +100,7 @@ async function populateJumpBackIn() {
|
||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
||||
|
||||
worlds.forEach((world) => {
|
||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||
const instance = props.recentInstances.find((instance) => instance.id === world.instance_id)
|
||||
|
||||
if (!instance || !world.last_played) {
|
||||
return
|
||||
@@ -115,23 +115,23 @@ async function populateJumpBackIn() {
|
||||
})
|
||||
|
||||
const servers: {
|
||||
instancePath: string
|
||||
instanceId: string
|
||||
address: string
|
||||
}[] = worldItems
|
||||
.filter((item) => item.world.type === 'server' && item.instance)
|
||||
.map((item) => ({
|
||||
instancePath: item.instance.path,
|
||||
instanceId: item.instance.id,
|
||||
address: (item.world as ServerWorld).address,
|
||||
}))
|
||||
|
||||
// fetch protocol versions for all unique MC versions with server worlds
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instanceId))
|
||||
await Promise.all(
|
||||
[...uniqueServerInstances].map((path) =>
|
||||
get_profile_protocol_version(path)
|
||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||
[...uniqueServerInstances].map((instanceId) =>
|
||||
get_instance_protocol_version(instanceId)
|
||||
.then((protoVer) => (protocolVersions.value[instanceId] = protoVer))
|
||||
.catch(() => {
|
||||
console.error(`Failed to get profile protocol for: ${path} `)
|
||||
console.error(`Failed to get instance protocol for: ${instanceId} `)
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -145,14 +145,14 @@ async function populateJumpBackIn() {
|
||||
}
|
||||
})
|
||||
|
||||
servers.forEach(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
servers.forEach(({ instanceId, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instanceId], address),
|
||||
)
|
||||
}
|
||||
|
||||
const instanceItems: InstanceJumpBackInItem[] = []
|
||||
for (const instance of props.recentInstances) {
|
||||
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
||||
const worldItem = worldItems.find((item) => item.instance.id === instance.id)
|
||||
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
||||
continue
|
||||
}
|
||||
@@ -171,14 +171,14 @@ async function populateJumpBackIn() {
|
||||
.slice(0, MAX_JUMP_BACK_IN)
|
||||
}
|
||||
|
||||
function refreshServer(address: string, instancePath: string) {
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||
function refreshServer(address: string, instanceId: string) {
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instanceId], address)
|
||||
}
|
||||
|
||||
async function joinWorld(world: WorldWithProfile, instance?: GameInstance) {
|
||||
async function joinWorld(world: WorldWithInstance, instance?: GameInstance) {
|
||||
console.log(`Joining world ${getWorldIdentifier(world)}`)
|
||||
if (world.type === 'server') {
|
||||
await start_join_server(world.profile, world.address).catch(handleError)
|
||||
await start_join_server(world.instance_id, world.address).catch(handleError)
|
||||
if (instance) {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: instance.loader,
|
||||
@@ -187,13 +187,13 @@ async function joinWorld(world: WorldWithProfile, instance?: GameInstance) {
|
||||
})
|
||||
}
|
||||
} else if (world.type === 'singleplayer') {
|
||||
await start_join_singleplayer_world(world.profile, world.path).catch(handleError)
|
||||
await start_join_singleplayer_world(world.instance_id, world.path).catch(handleError)
|
||||
}
|
||||
}
|
||||
|
||||
async function playInstance(instance: GameInstance) {
|
||||
await run(instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
||||
await run(instance.id)
|
||||
.catch((err) => handleSevereError(err, { instanceId: instance.id }))
|
||||
.finally(() => {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: instance.loader,
|
||||
@@ -210,14 +210,14 @@ async function stopInstance(path: string) {
|
||||
})
|
||||
}
|
||||
|
||||
const currentProfile = ref<string>()
|
||||
const currentInstance = ref<string>()
|
||||
const currentWorld = ref<string>()
|
||||
|
||||
const unlistenProcesses = await process_listener(async () => {
|
||||
await checkProcesses()
|
||||
})
|
||||
|
||||
const unlistenProfiles = await profile_listener(async () => {
|
||||
const unlistenInstances = await instance_listener(async () => {
|
||||
await populateJumpBackIn().catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
@@ -227,18 +227,18 @@ const runningInstances = ref<string[]>([])
|
||||
|
||||
type ProcessMetadata = {
|
||||
uuid: string
|
||||
profile_path: string
|
||||
instance_id: string
|
||||
start_time: string
|
||||
}
|
||||
|
||||
const checkProcesses = async () => {
|
||||
const runningProcesses: ProcessMetadata[] = await get_all().catch(handleError)
|
||||
|
||||
const runningPaths = runningProcesses.map((x) => x.profile_path)
|
||||
const runningPaths = runningProcesses.map((x) => x.instance_id)
|
||||
|
||||
const stoppedInstances = runningInstances.value.filter((x) => !runningPaths.includes(x))
|
||||
if (currentProfile.value && stoppedInstances.includes(currentProfile.value)) {
|
||||
currentProfile.value = undefined
|
||||
if (currentInstance.value && stoppedInstances.includes(currentInstance.value)) {
|
||||
currentInstance.value = undefined
|
||||
currentWorld.value = undefined
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenProfiles()
|
||||
unlistenInstances()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -278,14 +278,14 @@ onUnmounted(() => {
|
||||
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||
<template
|
||||
v-for="item in jumpBackInItems"
|
||||
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||
:key="`${item.instance.id}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||
>
|
||||
<WorldItem
|
||||
v-if="item.type === 'world'"
|
||||
:world="item.world"
|
||||
:playing-instance="runningInstances.includes(item.instance.path)"
|
||||
:playing-instance="runningInstances.includes(item.instance.id)"
|
||||
:playing-world="
|
||||
currentProfile === item.instance.path && currentWorld === getWorldIdentifier(item.world)
|
||||
currentInstance === item.instance.id && currentWorld === getWorldIdentifier(item.world)
|
||||
"
|
||||
:refreshing="
|
||||
item.world.type === 'server'
|
||||
@@ -306,34 +306,34 @@ onUnmounted(() => {
|
||||
:rendered-motd="
|
||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||
"
|
||||
:current-protocol="protocolVersions[item.instance.path]"
|
||||
:current-protocol="protocolVersions[item.instance.id]"
|
||||
:game-mode="
|
||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||
"
|
||||
:instance-path="item.instance.path"
|
||||
:instance-id="item.instance.id"
|
||||
:instance-name="item.instance.name"
|
||||
:instance-icon="item.instance.icon_path"
|
||||
@refresh="
|
||||
() =>
|
||||
item.world.type === 'server'
|
||||
? refreshServer(item.world.address, item.instance.path)
|
||||
? refreshServer(item.world.address, item.instance.id)
|
||||
: {}
|
||||
"
|
||||
@update="() => populateJumpBackIn()"
|
||||
@play="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
currentInstance = item.instance.id
|
||||
currentWorld = getWorldIdentifier(item.world)
|
||||
joinWorld(item.world, item.instance)
|
||||
}
|
||||
"
|
||||
@play-instance="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
currentInstance = item.instance.id
|
||||
playInstance(item.instance)
|
||||
}
|
||||
"
|
||||
@stop="() => stopInstance(item.instance.path)"
|
||||
@stop="() => stopInstance(item.instance.id)"
|
||||
/>
|
||||
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
ClipboardCopyIcon,
|
||||
EditIcon,
|
||||
ExternalIcon,
|
||||
EyeIcon,
|
||||
FolderOpenIcon,
|
||||
IssuesIcon,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
TagItem,
|
||||
@@ -39,7 +41,7 @@ import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { copyToClipboard } from '@/helpers/utils'
|
||||
import { copyToClipboard, createInstanceShortcut } from '@/helpers/utils'
|
||||
import type {
|
||||
ProtocolVersion,
|
||||
ServerStatus,
|
||||
@@ -60,6 +62,7 @@ const formatDateTime = useFormatDateTime({
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
@@ -91,9 +94,10 @@ const props = withDefaults(
|
||||
managed?: boolean
|
||||
|
||||
// Instance
|
||||
instancePath?: string
|
||||
instanceId?: string
|
||||
instanceName?: string
|
||||
instanceIcon?: string
|
||||
shortcutInstanceId?: string
|
||||
}>(),
|
||||
{
|
||||
playingInstance: false,
|
||||
@@ -110,9 +114,10 @@ const props = withDefaults(
|
||||
gameMode: undefined,
|
||||
managed: false,
|
||||
|
||||
instancePath: undefined,
|
||||
instanceId: undefined,
|
||||
instanceName: undefined,
|
||||
instanceIcon: undefined,
|
||||
shortcutInstanceId: undefined,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -131,6 +136,33 @@ const serverIncompatible = computed(
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
const managed = computed(() => props.managed)
|
||||
const shortcutInstanceId = computed(() => props.shortcutInstanceId ?? props.instanceId)
|
||||
|
||||
async function createShortcut() {
|
||||
if (!shortcutInstanceId.value) return
|
||||
|
||||
try {
|
||||
const shortcutPath = await createInstanceShortcut(
|
||||
props.world.name,
|
||||
shortcutInstanceId.value,
|
||||
props.world.type === 'server'
|
||||
? { server: (props.world as ServerWorld).address }
|
||||
: { singleplayerWorld: (props.world as SingleplayerWorld).path },
|
||||
)
|
||||
if (!shortcutPath) return
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Shortcut created',
|
||||
})
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Failed to create shortcut',
|
||||
text: error instanceof Error ? error.message : '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
hardcore: {
|
||||
@@ -185,6 +217,10 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.dont_show_on_home',
|
||||
defaultMessage: `Don't show on Home`,
|
||||
},
|
||||
createShortcut: {
|
||||
id: 'instance.worlds.create_shortcut',
|
||||
defaultMessage: 'Create shortcut',
|
||||
},
|
||||
linkedServer: {
|
||||
id: 'instance.worlds.linked_server',
|
||||
defaultMessage: 'Managed by server project',
|
||||
@@ -209,10 +245,10 @@ const messages = defineMessages({
|
||||
</script>
|
||||
<template>
|
||||
<SmartClickable>
|
||||
<template v-if="instancePath" #clickable>
|
||||
<template v-if="instanceId" #clickable>
|
||||
<router-link
|
||||
class="no-click-animation"
|
||||
:to="`/instance/${encodeURIComponent(instancePath)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
|
||||
:to="`/instance/${encodeURIComponent(instanceId)}/worlds?highlight=${encodeURIComponent(getWorldIdentifier(world))}`"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
@@ -324,16 +360,16 @@ const messages = defineMessages({
|
||||
</template>
|
||||
<template v-else> {{ formatMessage(messages.notPlayedYet) }} </template>
|
||||
</div>
|
||||
<template v-if="instancePath">
|
||||
<template v-if="instanceId">
|
||||
•
|
||||
<router-link
|
||||
class="flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||
:to="`/instance/${instancePath}`"
|
||||
:to="`/instance/${instanceId}`"
|
||||
>
|
||||
<Avatar
|
||||
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
||||
size="16px"
|
||||
:tint-by="instancePath"
|
||||
:tint-by="instanceId"
|
||||
class="shrink-0"
|
||||
/>
|
||||
<span class="truncate">{{ instanceName }}</span>
|
||||
@@ -420,14 +456,14 @@ const messages = defineMessages({
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
shown: !!instancePath,
|
||||
shown: !!instanceId,
|
||||
disabled: playingInstance,
|
||||
action: () => emit('play-instance'),
|
||||
},
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instancePath,
|
||||
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
||||
shown: !!instanceId,
|
||||
action: () => router.push(`/instance/${encodeURIComponent(instanceId)}`),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
@@ -442,7 +478,7 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'edit',
|
||||
action: () => emit('edit'),
|
||||
shown: !instancePath,
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
@@ -457,14 +493,14 @@ const messages = defineMessages({
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !!instancePath,
|
||||
shown: !!instanceId,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
shown: !!instancePath,
|
||||
shown: !!instanceId,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instancePath,
|
||||
instanceId,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
@@ -473,16 +509,21 @@ const messages = defineMessages({
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
shown: !!shortcutInstanceId,
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !instancePath,
|
||||
shown: !instanceId,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
action: () => emit('delete'),
|
||||
shown: !instancePath,
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
@@ -517,6 +558,10 @@ const messages = defineMessages({
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #create-shortcut>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createShortcut) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
|
||||
@@ -12,7 +12,11 @@ import { ref } from 'vue'
|
||||
|
||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
add_server_to_instance,
|
||||
type ServerPackStatus,
|
||||
type ServerWorld,
|
||||
} from '@/helpers/worlds.ts'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -35,8 +39,8 @@ async function addServer(play: boolean) {
|
||||
const serverName = name.value ? name.value : address.value
|
||||
const resourcePackStatus = resourcePack.value
|
||||
const index =
|
||||
(await add_server_to_profile(
|
||||
props.instance.path,
|
||||
(await add_server_to_instance(
|
||||
props.instance.id,
|
||||
serverName,
|
||||
address.value,
|
||||
resourcePackStatus,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { World } from '@/helpers/worlds.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
world: World | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [world: World]
|
||||
}>()
|
||||
|
||||
const messages = defineMessages({
|
||||
removeServerTitle: {
|
||||
id: 'app.instance.worlds.remove-server-modal.title',
|
||||
defaultMessage: 'Remove server',
|
||||
},
|
||||
deleteWorldTitle: {
|
||||
id: 'app.instance.worlds.delete-world-modal.title',
|
||||
defaultMessage: 'Delete world',
|
||||
},
|
||||
removeServerWarningHeader: {
|
||||
id: 'app.instance.worlds.remove-server-modal.warning-header',
|
||||
defaultMessage: 'Removing {name}',
|
||||
},
|
||||
deleteWorldWarningHeader: {
|
||||
id: 'app.instance.worlds.delete-world-modal.warning-header',
|
||||
defaultMessage: 'Deleting {name}',
|
||||
},
|
||||
removeServerWarningBody: {
|
||||
id: 'app.instance.worlds.remove-server-modal.warning-body',
|
||||
defaultMessage:
|
||||
'This server will be removed from your server list and from the in-game server list. You can add it again later if you know the address.',
|
||||
},
|
||||
deleteWorldWarningBody: {
|
||||
id: 'app.instance.worlds.delete-world-modal.warning-body',
|
||||
defaultMessage:
|
||||
'This world will be permanently deleted from this instance. This action cannot be undone.',
|
||||
},
|
||||
removeServerButton: {
|
||||
id: 'app.instance.worlds.remove-server-modal.remove-button',
|
||||
defaultMessage: 'Remove server',
|
||||
},
|
||||
deleteWorldButton: {
|
||||
id: 'app.instance.worlds.delete-world-modal.delete-button',
|
||||
defaultMessage: 'Delete world',
|
||||
},
|
||||
})
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
const isServer = computed(() => props.world?.type === 'server')
|
||||
const isSingleplayer = computed(() => props.world?.type === 'singleplayer')
|
||||
const titleMessage = computed(() =>
|
||||
isServer.value ? messages.removeServerTitle : messages.deleteWorldTitle,
|
||||
)
|
||||
const actionMessage = computed(() =>
|
||||
isServer.value ? messages.removeServerButton : messages.deleteWorldButton,
|
||||
)
|
||||
const warningHeaderMessage = computed(() =>
|
||||
isServer.value ? messages.removeServerWarningHeader : messages.deleteWorldWarningHeader,
|
||||
)
|
||||
const warningBodyMessage = computed(() =>
|
||||
isServer.value ? messages.removeServerWarningBody : messages.deleteWorldWarningBody,
|
||||
)
|
||||
|
||||
function show() {
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (!props.world) return
|
||||
emit('confirm', props.world)
|
||||
hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal ref="modal" :header="formatMessage(titleMessage)" fade="danger" max-width="500px">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Admonition
|
||||
type="critical"
|
||||
:header="formatMessage(warningHeaderMessage, { name: world?.name })"
|
||||
>
|
||||
{{ formatMessage(warningBodyMessage) }}
|
||||
</Admonition>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="!isServer && !isSingleplayer" @click="confirm">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(actionMessage) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
@@ -15,7 +15,7 @@ import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import {
|
||||
type DisplayStatus,
|
||||
edit_server_in_profile,
|
||||
edit_server_in_instance,
|
||||
type ServerPackStatus,
|
||||
type ServerWorld,
|
||||
set_world_display_status,
|
||||
@@ -46,8 +46,8 @@ const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal
|
||||
async function saveServer() {
|
||||
const serverName = name.value ? name.value : address.value
|
||||
const resourcePackStatus = resourcePack.value
|
||||
await edit_server_in_profile(
|
||||
props.instance.path,
|
||||
await edit_server_in_instance(
|
||||
props.instance.id,
|
||||
index.value,
|
||||
serverName,
|
||||
address.value,
|
||||
@@ -56,7 +56,7 @@ async function saveServer() {
|
||||
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
props.instance.id,
|
||||
'server',
|
||||
address.value,
|
||||
newDisplayStatus.value,
|
||||
|
||||
@@ -40,14 +40,14 @@ const hideFromHome = ref(false)
|
||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||
|
||||
async function saveWorld() {
|
||||
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
||||
await rename_world(props.instance.id, path.value, name.value).catch(handleError)
|
||||
|
||||
if (removeIcon.value) {
|
||||
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
||||
await reset_world_icon(props.instance.id, path.value).catch(handleError)
|
||||
}
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
props.instance.id,
|
||||
'singleplayer',
|
||||
path.value,
|
||||
newDisplayStatus.value,
|
||||
|
||||
@@ -0,0 +1,752 @@
|
||||
import { CheckIcon, CopyIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import {
|
||||
defineMessages,
|
||||
type PopupNotificationButton,
|
||||
type PopupNotificationProgressItem,
|
||||
type PopupNotificationProgressType,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { install_job_listener } from '@/helpers/events'
|
||||
import {
|
||||
install_job_dismiss,
|
||||
install_job_list,
|
||||
install_job_retry,
|
||||
install_job_support_details,
|
||||
installJobInstanceId,
|
||||
type InstallJobSnapshot,
|
||||
type InstallJobStatus,
|
||||
type InstallPhaseId,
|
||||
type InstallProgress,
|
||||
} from '@/helpers/install'
|
||||
import { get_many as getInstances } from '@/helpers/instance'
|
||||
|
||||
const messages = defineMessages({
|
||||
installs: {
|
||||
id: 'app.action-bar.installs',
|
||||
defaultMessage: 'Installs',
|
||||
},
|
||||
retry: {
|
||||
id: 'app.action-bar.install.retry',
|
||||
defaultMessage: 'Retry',
|
||||
},
|
||||
copyDetails: {
|
||||
id: 'app.action-bar.install.copy-details',
|
||||
defaultMessage: 'Copy details',
|
||||
},
|
||||
copied: {
|
||||
id: 'app.action-bar.install.copied-details',
|
||||
defaultMessage: 'Copied',
|
||||
},
|
||||
dismiss: {
|
||||
id: 'app.action-bar.install.dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
},
|
||||
openInstance: {
|
||||
id: 'app.action-bar.install.open-instance',
|
||||
defaultMessage: 'Open instance',
|
||||
},
|
||||
unknownInstance: {
|
||||
id: 'app.action-bar.install.unknown-instance',
|
||||
defaultMessage: 'Unknown instance',
|
||||
},
|
||||
updatingSharedContent: {
|
||||
id: 'app.action-bar.install.updating-shared-content',
|
||||
defaultMessage: 'Updating shared content',
|
||||
},
|
||||
})
|
||||
|
||||
const phaseMessages = defineMessages({
|
||||
preparing_instance: {
|
||||
id: 'app.install.phase.preparing_instance',
|
||||
defaultMessage: 'Queued to install',
|
||||
},
|
||||
resolving_pack: {
|
||||
id: 'app.install.phase.resolving_pack',
|
||||
defaultMessage: 'Resolving content',
|
||||
},
|
||||
downloading_pack_file: {
|
||||
id: 'app.install.phase.downloading_pack_file',
|
||||
defaultMessage: 'Downloading pack file',
|
||||
},
|
||||
reading_pack_manifest: {
|
||||
id: 'app.install.phase.reading_pack_manifest',
|
||||
defaultMessage: 'Reading pack manifest',
|
||||
},
|
||||
downloading_content: {
|
||||
id: 'app.install.phase.downloading_content',
|
||||
defaultMessage: 'Downloading content',
|
||||
},
|
||||
extracting_overrides: {
|
||||
id: 'app.install.phase.extracting_overrides',
|
||||
defaultMessage: 'Extracting overrides',
|
||||
},
|
||||
resolving_minecraft: {
|
||||
id: 'app.install.phase.resolving_minecraft',
|
||||
defaultMessage: 'Resolving Minecraft',
|
||||
},
|
||||
resolving_loader: {
|
||||
id: 'app.install.phase.resolving_loader',
|
||||
defaultMessage: 'Resolving loader',
|
||||
},
|
||||
preparing_java: {
|
||||
id: 'app.install.phase.preparing_java',
|
||||
defaultMessage: 'Preparing Java',
|
||||
},
|
||||
downloading_minecraft: {
|
||||
id: 'app.install.phase.downloading_minecraft',
|
||||
defaultMessage: 'Downloading Minecraft',
|
||||
},
|
||||
running_loader_processors: {
|
||||
id: 'app.install.phase.running_loader_processors',
|
||||
defaultMessage: 'Running loader processors',
|
||||
},
|
||||
finalizing: {
|
||||
id: 'app.install.phase.finalizing',
|
||||
defaultMessage: 'Finalizing',
|
||||
},
|
||||
rolling_back: {
|
||||
id: 'app.install.phase.rolling_back',
|
||||
defaultMessage: 'Rolling back',
|
||||
},
|
||||
})
|
||||
|
||||
const javaStepMessages = defineMessages({
|
||||
resolving: {
|
||||
id: 'app.install.phase.preparing_java.resolving',
|
||||
defaultMessage: 'Preparing Java {version}',
|
||||
},
|
||||
fetching_metadata: {
|
||||
id: 'app.install.phase.preparing_java.fetching-metadata',
|
||||
defaultMessage: 'Fetching Java {version}',
|
||||
},
|
||||
downloading: {
|
||||
id: 'app.install.phase.preparing_java.downloading',
|
||||
defaultMessage: 'Downloading Java {version}',
|
||||
},
|
||||
extracting: {
|
||||
id: 'app.install.phase.preparing_java.extracting',
|
||||
defaultMessage: 'Extracting Java {version}',
|
||||
},
|
||||
validating: {
|
||||
id: 'app.install.phase.preparing_java.validating',
|
||||
defaultMessage: 'Validating Java {version}',
|
||||
},
|
||||
})
|
||||
|
||||
const failureSummaryMessages = defineMessages({
|
||||
canceled: {
|
||||
id: 'app.action-bar.install.summary.canceled',
|
||||
defaultMessage: 'Canceled',
|
||||
},
|
||||
appClosed: {
|
||||
id: 'app.action-bar.install.summary.app-closing',
|
||||
defaultMessage: 'Canceled due to app closing',
|
||||
},
|
||||
downloadFailed: {
|
||||
id: 'app.action-bar.install.summary.download-failed',
|
||||
defaultMessage: "Download couldn't finish",
|
||||
},
|
||||
modrinthUnreachable: {
|
||||
id: 'app.action-bar.install.summary.modrinth-unreachable',
|
||||
defaultMessage: "Couldn't reach Modrinth",
|
||||
},
|
||||
packDownloadFailed: {
|
||||
id: 'app.action-bar.install.summary.pack-download-failed',
|
||||
defaultMessage: "Couldn't download pack",
|
||||
},
|
||||
badModpackFile: {
|
||||
id: 'app.action-bar.install.summary.bad-modpack-file',
|
||||
defaultMessage: "Couldn't read modpack",
|
||||
},
|
||||
invalidModpack: {
|
||||
id: 'app.action-bar.install.summary.invalid-modpack',
|
||||
defaultMessage: 'Modpack data invalid',
|
||||
},
|
||||
contentDownloadFailed: {
|
||||
id: 'app.action-bar.install.summary.content-download-failed',
|
||||
defaultMessage: "Couldn't download files",
|
||||
},
|
||||
corruptDownload: {
|
||||
id: 'app.action-bar.install.summary.corrupt-download',
|
||||
defaultMessage: 'Downloaded file is corrupt',
|
||||
},
|
||||
invalidModpackFiles: {
|
||||
id: 'app.action-bar.install.summary.invalid-modpack-files',
|
||||
defaultMessage: 'Modpack files have invalid metadata',
|
||||
},
|
||||
noWritePermission: {
|
||||
id: 'app.action-bar.install.summary.no-write-permission',
|
||||
defaultMessage: 'No permission to write',
|
||||
},
|
||||
couldNotSaveFiles: {
|
||||
id: 'app.action-bar.install.summary.could-not-save-files',
|
||||
defaultMessage: "Couldn't save files",
|
||||
},
|
||||
invalidFilePath: {
|
||||
id: 'app.action-bar.install.summary.invalid-file-path',
|
||||
defaultMessage: 'File path is invalid',
|
||||
},
|
||||
instanceNotFound: {
|
||||
id: 'app.action-bar.install.summary.instance-not-found',
|
||||
defaultMessage: "Instance couldn't be found",
|
||||
},
|
||||
cleanupIncomplete: {
|
||||
id: 'app.action-bar.install.summary.cleanup-incomplete',
|
||||
defaultMessage: "Cleanup didn't finish",
|
||||
},
|
||||
javaSetupFailed: {
|
||||
id: 'app.action-bar.install.summary.java-setup-failed',
|
||||
defaultMessage: "Java setup couldn't finish",
|
||||
},
|
||||
minecraftSetupFailed: {
|
||||
id: 'app.action-bar.install.summary.minecraft-setup-failed',
|
||||
defaultMessage: 'Minecraft setup failed',
|
||||
},
|
||||
loaderSetupFailed: {
|
||||
id: 'app.action-bar.install.summary.loader-setup-failed',
|
||||
defaultMessage: 'Loader setup failed',
|
||||
},
|
||||
localDataError: {
|
||||
id: 'app.action-bar.install.summary.local-data-error',
|
||||
defaultMessage: "Couldn't update local data",
|
||||
},
|
||||
unexpectedError: {
|
||||
id: 'app.action-bar.install.summary.unexpected-error',
|
||||
defaultMessage: 'Something went wrong',
|
||||
},
|
||||
})
|
||||
|
||||
const visibleJobStatuses = new Set<InstallJobStatus>(['queued', 'running', 'failed', 'interrupted'])
|
||||
const copyDetailsStallMs = 30_000
|
||||
|
||||
interface ProgressSnapshot {
|
||||
signature: string
|
||||
changedAt: number
|
||||
timeout: number | null
|
||||
}
|
||||
|
||||
function getDisplayIconUrl(icon: string | null | undefined): string | null {
|
||||
if (!icon) return null
|
||||
if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) return icon
|
||||
return convertFileSrc(icon)
|
||||
}
|
||||
|
||||
export async function useInstallJobNotifications(opts: {
|
||||
router: Router
|
||||
handleError: (err: unknown) => void
|
||||
onChange: () => void
|
||||
}) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const jobs = ref<InstallJobSnapshot[]>([])
|
||||
const iconUrls = ref<Record<string, string | null>>({})
|
||||
const instanceNames = ref<Record<string, string>>({})
|
||||
const copiedJobIds = ref<Set<string>>(new Set())
|
||||
const jobOrder = new Map<string, number>()
|
||||
let refreshRequest = 0
|
||||
let metadataRequest = 0
|
||||
let nextJobOrder = 0
|
||||
const copiedResetTimeouts = new Map<string, number>()
|
||||
const progressSnapshots = new Map<string, ProgressSnapshot>()
|
||||
|
||||
function getTitle(job: InstallJobSnapshot): string {
|
||||
if (job.display?.title) return job.display.title
|
||||
if (job.details.type === 'instance') return job.details.name
|
||||
if (job.details.type === 'modpack' && job.details.title) return job.details.title
|
||||
const instanceId = installJobInstanceId(job)
|
||||
return (
|
||||
(instanceId ? instanceNames.value[instanceId] : null) ??
|
||||
formatMessage(messages.unknownInstance)
|
||||
)
|
||||
}
|
||||
|
||||
function getText(job: InstallJobSnapshot): string {
|
||||
if (job.status === 'failed' || job.status === 'interrupted') {
|
||||
return getFailureSummary(job)
|
||||
}
|
||||
if (job.phase === 'preparing_java' && job.details.type === 'java') {
|
||||
return formatMessage(javaStepMessages[job.details.step], {
|
||||
version: job.details.major_version,
|
||||
})
|
||||
}
|
||||
if (job.kind === 'update_shared_instance' && job.phase === 'downloading_content') {
|
||||
return formatMessage(messages.updatingSharedContent)
|
||||
}
|
||||
return formatMessage(phaseMessages[job.phase])
|
||||
}
|
||||
|
||||
function getFailureSummary(job: InstallJobSnapshot): string {
|
||||
const code = job.error?.code
|
||||
const phase = job.error?.phase ?? job.phase
|
||||
|
||||
if (code === 'app_closed' || (job.status === 'interrupted' && code === 'interrupted')) {
|
||||
return formatMessage(failureSummaryMessages.appClosed)
|
||||
}
|
||||
if (code === 'canceled') {
|
||||
return formatMessage(failureSummaryMessages.canceled)
|
||||
}
|
||||
if (job.rollback_error || code === 'rollback_error') {
|
||||
return formatMessage(failureSummaryMessages.cleanupIncomplete)
|
||||
}
|
||||
if (hasPermissionError(job)) {
|
||||
return formatMessage(failureSummaryMessages.noWritePermission)
|
||||
}
|
||||
|
||||
switch (code) {
|
||||
case 'network_error':
|
||||
return formatMessage(
|
||||
phase === 'downloading_pack_file'
|
||||
? failureSummaryMessages.packDownloadFailed
|
||||
: failureSummaryMessages.downloadFailed,
|
||||
)
|
||||
case 'api_error':
|
||||
return formatMessage(failureSummaryMessages.modrinthUnreachable)
|
||||
case 'pack_error':
|
||||
return formatMessage(
|
||||
phase === 'downloading_pack_file'
|
||||
? failureSummaryMessages.packDownloadFailed
|
||||
: failureSummaryMessages.invalidModpack,
|
||||
)
|
||||
case 'archive_error':
|
||||
return formatMessage(failureSummaryMessages.badModpackFile)
|
||||
case 'parse_error':
|
||||
return formatMessage(failureSummaryMessages.invalidModpack)
|
||||
case 'content_error':
|
||||
return formatMessage(failureSummaryMessages.invalidModpackFiles)
|
||||
case 'hash_error':
|
||||
return formatMessage(failureSummaryMessages.corruptDownload)
|
||||
case 'filesystem_error':
|
||||
return formatMessage(failureSummaryMessages.couldNotSaveFiles)
|
||||
case 'path_error':
|
||||
return formatMessage(failureSummaryMessages.invalidFilePath)
|
||||
case 'instance_error':
|
||||
return formatMessage(failureSummaryMessages.instanceNotFound)
|
||||
case 'java_error':
|
||||
return formatMessage(failureSummaryMessages.javaSetupFailed)
|
||||
case 'loader_error':
|
||||
case 'processor_error':
|
||||
return formatMessage(failureSummaryMessages.loaderSetupFailed)
|
||||
case 'database_error':
|
||||
return formatMessage(failureSummaryMessages.localDataError)
|
||||
case 'launcher_error':
|
||||
case 'metadata_error':
|
||||
return getFailureSummaryForPhase(phase)
|
||||
default:
|
||||
return getFailureSummaryForPhase(phase)
|
||||
}
|
||||
}
|
||||
|
||||
function getFailureSummaryForPhase(phase: InstallPhaseId): string {
|
||||
switch (phase) {
|
||||
case 'downloading_pack_file':
|
||||
return formatMessage(failureSummaryMessages.packDownloadFailed)
|
||||
case 'resolving_pack':
|
||||
case 'reading_pack_manifest':
|
||||
return formatMessage(failureSummaryMessages.invalidModpack)
|
||||
case 'downloading_content':
|
||||
return formatMessage(failureSummaryMessages.contentDownloadFailed)
|
||||
case 'extracting_overrides':
|
||||
return formatMessage(failureSummaryMessages.couldNotSaveFiles)
|
||||
case 'resolving_minecraft':
|
||||
case 'downloading_minecraft':
|
||||
return formatMessage(failureSummaryMessages.minecraftSetupFailed)
|
||||
case 'resolving_loader':
|
||||
case 'running_loader_processors':
|
||||
return formatMessage(failureSummaryMessages.loaderSetupFailed)
|
||||
case 'preparing_java':
|
||||
return formatMessage(failureSummaryMessages.javaSetupFailed)
|
||||
case 'preparing_instance':
|
||||
return formatMessage(failureSummaryMessages.instanceNotFound)
|
||||
case 'rolling_back':
|
||||
return formatMessage(failureSummaryMessages.cleanupIncomplete)
|
||||
default:
|
||||
return formatMessage(failureSummaryMessages.unexpectedError)
|
||||
}
|
||||
}
|
||||
|
||||
function hasPermissionError(job: InstallJobSnapshot): boolean {
|
||||
const message = job.error?.message.toLowerCase() ?? ''
|
||||
return (
|
||||
message.includes('permission denied') ||
|
||||
message.includes('access is denied') ||
|
||||
message.includes('operation not permitted')
|
||||
)
|
||||
}
|
||||
|
||||
function getProgressType(job: InstallJobSnapshot): PopupNotificationProgressType | undefined {
|
||||
if (!getEffectiveProgress(job)) return undefined
|
||||
if (
|
||||
job.phase === 'preparing_java' &&
|
||||
job.details.type === 'java' &&
|
||||
job.details.step === 'downloading'
|
||||
) {
|
||||
return 'bytes'
|
||||
}
|
||||
if (job.phase === 'downloading_content') {
|
||||
return job.progress?.secondary ? 'bytes' : 'count'
|
||||
}
|
||||
if (
|
||||
job.phase === 'downloading_pack_file' ||
|
||||
job.phase === 'extracting_overrides' ||
|
||||
job.phase === 'downloading_minecraft'
|
||||
) {
|
||||
return 'bytes'
|
||||
}
|
||||
if (job.phase === 'running_loader_processors') {
|
||||
return 'count'
|
||||
}
|
||||
return 'percentage'
|
||||
}
|
||||
|
||||
function getEffectiveProgress(job: InstallJobSnapshot): InstallProgress | null | undefined {
|
||||
if (job.phase === 'downloading_content' && job.progress?.secondary) {
|
||||
return job.progress.secondary
|
||||
}
|
||||
|
||||
return job.progress
|
||||
}
|
||||
|
||||
function getProgress(job: InstallJobSnapshot): number {
|
||||
if (job.status === 'succeeded') return 1
|
||||
if (job.status === 'failed' || job.status === 'interrupted') return 0
|
||||
const progress = getEffectiveProgress(job)
|
||||
if (!progress || progress.total <= 0) return 0
|
||||
return Math.max(0, Math.min(1, progress.current / progress.total))
|
||||
}
|
||||
|
||||
function isTerminalJob(job: InstallJobSnapshot): boolean {
|
||||
return job.status === 'failed' || job.status === 'interrupted'
|
||||
}
|
||||
|
||||
function canShowStalledProgressDetails(job: InstallJobSnapshot): boolean {
|
||||
return (
|
||||
job.status === 'running' &&
|
||||
job.phase !== 'preparing_instance' &&
|
||||
job.phase !== 'finalizing' &&
|
||||
job.phase !== 'rolling_back'
|
||||
)
|
||||
}
|
||||
|
||||
function getJobSortRank(job: InstallJobSnapshot): number {
|
||||
if (isTerminalJob(job)) return 0
|
||||
if (job.status === 'queued' || job.phase === 'preparing_instance') return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
function progressSignature(job: InstallJobSnapshot): string {
|
||||
const progress = job.progress
|
||||
const secondary = progress?.secondary
|
||||
return [
|
||||
job.status,
|
||||
job.phase,
|
||||
JSON.stringify(job.details),
|
||||
progress?.current ?? '',
|
||||
progress?.total ?? '',
|
||||
secondary?.current ?? '',
|
||||
secondary?.total ?? '',
|
||||
].join(':')
|
||||
}
|
||||
|
||||
function clearCopied(jobId: string) {
|
||||
if (!copiedJobIds.value.has(jobId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = copiedResetTimeouts.get(jobId)
|
||||
if (timeout != null) {
|
||||
window.clearTimeout(timeout)
|
||||
copiedResetTimeouts.delete(jobId)
|
||||
}
|
||||
|
||||
const nextCopiedJobIds = new Set(copiedJobIds.value)
|
||||
nextCopiedJobIds.delete(jobId)
|
||||
copiedJobIds.value = nextCopiedJobIds
|
||||
}
|
||||
|
||||
function clearProgressSnapshot(jobId: string) {
|
||||
const snapshot = progressSnapshots.get(jobId)
|
||||
if (snapshot?.timeout != null) {
|
||||
window.clearTimeout(snapshot.timeout)
|
||||
}
|
||||
progressSnapshots.delete(jobId)
|
||||
}
|
||||
|
||||
function scheduleStaleProgressRefresh(jobId: string) {
|
||||
const snapshot = progressSnapshots.get(jobId)
|
||||
if (!snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.timeout = window.setTimeout(() => {
|
||||
const snapshot = progressSnapshots.get(jobId)
|
||||
if (!snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.timeout = null
|
||||
opts.onChange()
|
||||
}, copyDetailsStallMs)
|
||||
}
|
||||
|
||||
function syncProgressSnapshots(nextJobs: InstallJobSnapshot[]) {
|
||||
const trackedJobIds = new Set<string>()
|
||||
const now = Date.now()
|
||||
|
||||
for (const job of nextJobs) {
|
||||
if (!canShowStalledProgressDetails(job)) {
|
||||
continue
|
||||
}
|
||||
|
||||
trackedJobIds.add(job.job_id)
|
||||
const signature = progressSignature(job)
|
||||
const snapshot = progressSnapshots.get(job.job_id)
|
||||
if (snapshot?.signature === signature) {
|
||||
continue
|
||||
}
|
||||
|
||||
clearProgressSnapshot(job.job_id)
|
||||
clearCopied(job.job_id)
|
||||
progressSnapshots.set(job.job_id, {
|
||||
signature,
|
||||
changedAt: now,
|
||||
timeout: null,
|
||||
})
|
||||
scheduleStaleProgressRefresh(job.job_id)
|
||||
}
|
||||
|
||||
for (const jobId of progressSnapshots.keys()) {
|
||||
if (!trackedJobIds.has(jobId)) {
|
||||
clearProgressSnapshot(jobId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasStalledProgress(job: InstallJobSnapshot): boolean {
|
||||
const snapshot = progressSnapshots.get(job.job_id)
|
||||
return !!snapshot && Date.now() - snapshot.changedAt >= copyDetailsStallMs
|
||||
}
|
||||
|
||||
function shouldShowCopyDetails(job: InstallJobSnapshot): boolean {
|
||||
return isTerminalJob(job) || (canShowStalledProgressDetails(job) && hasStalledProgress(job))
|
||||
}
|
||||
|
||||
function isCopied(job: InstallJobSnapshot): boolean {
|
||||
return copiedJobIds.value.has(job.job_id)
|
||||
}
|
||||
|
||||
function setCopied(job: InstallJobSnapshot) {
|
||||
copiedJobIds.value = new Set([...copiedJobIds.value, job.job_id])
|
||||
const existingTimeout = copiedResetTimeouts.get(job.job_id)
|
||||
if (existingTimeout != null) {
|
||||
window.clearTimeout(existingTimeout)
|
||||
}
|
||||
copiedResetTimeouts.set(
|
||||
job.job_id,
|
||||
window.setTimeout(() => {
|
||||
copiedResetTimeouts.delete(job.job_id)
|
||||
if (!copiedJobIds.value.has(job.job_id)) {
|
||||
return
|
||||
}
|
||||
const nextCopiedJobIds = new Set(copiedJobIds.value)
|
||||
nextCopiedJobIds.delete(job.job_id)
|
||||
copiedJobIds.value = nextCopiedJobIds
|
||||
opts.onChange()
|
||||
}, 1_000),
|
||||
)
|
||||
opts.onChange()
|
||||
}
|
||||
|
||||
async function copyJobDetails(job: InstallJobSnapshot) {
|
||||
const details = await install_job_support_details(job.job_id).catch((error) => {
|
||||
opts.handleError(error)
|
||||
return null
|
||||
})
|
||||
if (!details) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(details)
|
||||
setCopied(job)
|
||||
} catch (error) {
|
||||
opts.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
function getButtons(job: InstallJobSnapshot): PopupNotificationButton[] {
|
||||
const buttons: PopupNotificationButton[] = []
|
||||
|
||||
if (isTerminalJob(job)) {
|
||||
buttons.push({
|
||||
label: formatMessage(messages.retry),
|
||||
icon: UpdatedIcon,
|
||||
color: 'brand',
|
||||
keepOpen: true,
|
||||
action: async () => {
|
||||
await install_job_retry(job.job_id).catch(opts.handleError)
|
||||
await refresh()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (shouldShowCopyDetails(job)) {
|
||||
const copied = isCopied(job)
|
||||
buttons.push({
|
||||
label: formatMessage(copied ? messages.copied : messages.copyDetails),
|
||||
icon: copied ? CheckIcon : CopyIcon,
|
||||
color: 'standard',
|
||||
keepOpen: true,
|
||||
action: async () => {
|
||||
await copyJobDetails(job)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
function setJobs(nextJobs: InstallJobSnapshot[]) {
|
||||
for (const job of nextJobs) {
|
||||
if (!jobOrder.has(job.job_id)) {
|
||||
jobOrder.set(job.job_id, nextJobOrder++)
|
||||
}
|
||||
}
|
||||
|
||||
const visibleJobs = nextJobs.filter((job) => visibleJobStatuses.has(job.status))
|
||||
syncProgressSnapshots(visibleJobs)
|
||||
|
||||
jobs.value = visibleJobs.sort(
|
||||
(a, b) =>
|
||||
getJobSortRank(a) - getJobSortRank(b) ||
|
||||
a.created.localeCompare(b.created) ||
|
||||
(jobOrder.get(a.job_id) ?? 0) - (jobOrder.get(b.job_id) ?? 0),
|
||||
)
|
||||
}
|
||||
|
||||
const progressItems = computed<PopupNotificationProgressItem[]>(() =>
|
||||
jobs.value.map((job) => {
|
||||
const progress = getEffectiveProgress(job)
|
||||
|
||||
return {
|
||||
id: job.job_id,
|
||||
title: getTitle(job),
|
||||
text: getText(job),
|
||||
iconUrl: iconUrls.value[job.job_id] ?? null,
|
||||
progress: getProgress(job),
|
||||
waiting: !job.progress && ['queued', 'running'].includes(job.status),
|
||||
showProgress: !isTerminalJob(job),
|
||||
wrapText: isTerminalJob(job),
|
||||
progressType: isTerminalJob(job) ? undefined : getProgressType(job),
|
||||
progressCurrent: isTerminalJob(job) ? undefined : progress?.current,
|
||||
progressTotal: isTerminalJob(job) ? undefined : progress?.total,
|
||||
buttons: getButtons(job),
|
||||
onDismiss: isTerminalJob(job)
|
||||
? async () => {
|
||||
await install_job_dismiss(job.job_id).catch(opts.handleError)
|
||||
await refresh()
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const buttons = computed<PopupNotificationButton[] | undefined>(() => undefined)
|
||||
|
||||
async function refreshMetadata(notify = true) {
|
||||
const request = ++metadataRequest
|
||||
const sourceJobs = jobs.value
|
||||
const instanceIds = Array.from(
|
||||
new Set(
|
||||
sourceJobs
|
||||
.map((job) => installJobInstanceId(job))
|
||||
.filter((instanceId): instanceId is string => !!instanceId),
|
||||
),
|
||||
)
|
||||
const instances = instanceIds.length
|
||||
? await getInstances(instanceIds).catch((error) => {
|
||||
opts.handleError(error)
|
||||
return []
|
||||
})
|
||||
: []
|
||||
|
||||
if (request !== metadataRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
const instanceIconUrls = new Map(
|
||||
instances.map((instance) => [instance.id, getDisplayIconUrl(instance.icon_path)]),
|
||||
)
|
||||
instanceNames.value = Object.fromEntries(
|
||||
instances.map((instance) => [instance.id, instance.name]),
|
||||
)
|
||||
iconUrls.value = Object.fromEntries(
|
||||
sourceJobs.map((job) => [
|
||||
job.job_id,
|
||||
getDisplayIconUrl(job.display?.icon) ??
|
||||
instanceIconUrls.get(installJobInstanceId(job) ?? '') ??
|
||||
null,
|
||||
]),
|
||||
)
|
||||
|
||||
if (notify) {
|
||||
opts.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh(notify = true) {
|
||||
const request = ++refreshRequest
|
||||
const nextJobs = await install_job_list(false).catch((error) => {
|
||||
opts.handleError(error)
|
||||
return []
|
||||
})
|
||||
|
||||
if (request !== refreshRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
setJobs(nextJobs)
|
||||
await refreshMetadata(false)
|
||||
|
||||
if (request !== refreshRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
opts.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
function applyJobUpdate(job: InstallJobSnapshot) {
|
||||
refreshRequest += 1
|
||||
const existingJob = jobs.value.find((item) => item.job_id === job.job_id)
|
||||
if (existingJob && existingJob.modified.localeCompare(job.modified) > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setJobs([...jobs.value.filter((item) => item.job_id !== job.job_id), job])
|
||||
opts.onChange()
|
||||
void refreshMetadata()
|
||||
}
|
||||
|
||||
await refresh(false)
|
||||
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
|
||||
|
||||
return {
|
||||
active: computed(() => jobs.value.length > 0),
|
||||
title: computed(() => formatMessage(messages.installs)),
|
||||
progressItems,
|
||||
buttons,
|
||||
refresh,
|
||||
dispose: () => {
|
||||
for (const timeout of copiedResetTimeouts.values()) {
|
||||
window.clearTimeout(timeout)
|
||||
}
|
||||
for (const jobId of progressSnapshots.keys()) {
|
||||
clearProgressSnapshot(jobId)
|
||||
}
|
||||
unlisten()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,24 @@ 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 { kill, list as listInstances } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
|
||||
import { getServerAddress } from '@/store/install.js'
|
||||
import { add_server_to_instance, getServerAddress } from '@/helpers/worlds'
|
||||
|
||||
interface BrowseServerInstance {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
@@ -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[]) {
|
||||
@@ -83,13 +87,11 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
})
|
||||
const newRunning: Record<string, string> = {}
|
||||
for (const hit of hits) {
|
||||
const inst = packs.find(
|
||||
(pack: GameInstance) => pack.linked_data?.project_id === hit.project_id,
|
||||
)
|
||||
const inst = packs.find((pack: GameInstance) => pack.link?.project_id === hit.project_id)
|
||||
if (inst) {
|
||||
const processes = await get_by_profile_path(inst.path).catch(() => [])
|
||||
const processes = await get_by_instance_id(inst.id).catch(() => [])
|
||||
if (Array.isArray(processes) && processes.length > 0) {
|
||||
newRunning[hit.project_id] = inst.path
|
||||
newRunning[hit.project_id] = inst.id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,9 +101,9 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
|
||||
async function handleStopServerProject(projectId: string) {
|
||||
debugLog('handleStopServerProject', projectId)
|
||||
const instancePath = runningServerProjects.value[projectId]
|
||||
if (!instancePath) return
|
||||
await kill(instancePath).catch(() => {})
|
||||
const instanceId = runningServerProjects.value[projectId]
|
||||
if (!instanceId) return
|
||||
await kill(instanceId).catch(() => {})
|
||||
const { [projectId]: _, ...rest } = runningServerProjects.value
|
||||
runningServerProjects.value = rest
|
||||
}
|
||||
@@ -118,9 +120,10 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
if (!address) return
|
||||
|
||||
if (options.instance.value) {
|
||||
const instanceId = options.instance.value.id
|
||||
try {
|
||||
await add_server_to_profile(
|
||||
options.instance.value.path,
|
||||
await add_server_to_instance(
|
||||
instanceId,
|
||||
project.name,
|
||||
address,
|
||||
'prompt',
|
||||
@@ -128,6 +131,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
project.minecraft_java_server?.content?.kind,
|
||||
)
|
||||
options.newlyInstalled.value.push(project.project_id)
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instanceId] })
|
||||
} catch (error) {
|
||||
options.handleError(error)
|
||||
}
|
||||
@@ -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 }
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -289,11 +282,11 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
process_listener((event: { event: string; profile_path_id: string }) => {
|
||||
process_listener((event: { event: string; instance_id: string }) => {
|
||||
debugLog('process event', event)
|
||||
if (event.event === 'finished') {
|
||||
const projectId = Object.entries(runningServerProjects.value).find(
|
||||
([, path]) => path === event.profile_path_id,
|
||||
([, path]) => path === event.instance_id,
|
||||
)?.[0]
|
||||
if (projectId) {
|
||||
const { [projectId]: _, ...rest } = runningServerProjects.value
|
||||
@@ -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,119 @@
|
||||
import { type ContentActionWarning, type ContentItem, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, type Ref } from 'vue'
|
||||
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
const managedSourceKinds = new Set(['shared_instance', 'modrinth_modpack', 'imported_modpack'])
|
||||
|
||||
export function useManagedContentPolicy(instance: Ref<GameInstance>) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const isManagedModpack = computed(() => instance.value.shared_instance?.role === 'member')
|
||||
const canUnpublish = computed(() => instance.value.shared_instance?.role === 'owner')
|
||||
const canUnlink = computed(() => instance.value.shared_instance?.role === 'member')
|
||||
const managedModpackWarning = computed(() => ({
|
||||
admonitionHeader: formatMessage(messages.warningHeader),
|
||||
changeVersionBody: formatMessage(messages.changeVersionBody),
|
||||
unlinkBody: formatMessage(messages.unlinkBody),
|
||||
}))
|
||||
|
||||
function isManagedContent(item: ContentItem) {
|
||||
return isManagedModpack.value && !!item.source_kind && managedSourceKinds.has(item.source_kind)
|
||||
}
|
||||
|
||||
function canMutateContent(item: ContentItem) {
|
||||
return !isManagedContent(item)
|
||||
}
|
||||
|
||||
function canUpdateContent(item: ContentItem) {
|
||||
return (
|
||||
canMutateContent(item) && !!item.file_path && !!item.has_update && !!item.update_version_id
|
||||
)
|
||||
}
|
||||
|
||||
function deleteWarning(items: ContentItem[]): ContentActionWarning | null {
|
||||
if (!items.some(isManagedContent)) return null
|
||||
return {
|
||||
admonitionHeader: formatMessage(messages.warningHeader),
|
||||
admonitionBody: formatMessage(
|
||||
items.length === 1 ? messages.deleteSingleBody : messages.deleteBulkBody,
|
||||
),
|
||||
actionLabel: formatMessage(
|
||||
items.length === 1 ? messages.deleteButton : messages.deleteManyButton,
|
||||
{ count: items.length },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function disableWarning(items: ContentItem[]): ContentActionWarning | null {
|
||||
if (!items.some(isManagedContent)) return null
|
||||
return {
|
||||
admonitionHeader: formatMessage(messages.warningHeader),
|
||||
admonitionBody: formatMessage(
|
||||
items.length === 1 ? messages.disableSingleBody : messages.disableBulkBody,
|
||||
),
|
||||
actionLabel: formatMessage(
|
||||
items.length === 1 ? messages.disableButton : messages.disableManyButton,
|
||||
{ count: items.length },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isManagedModpack,
|
||||
canUnpublish,
|
||||
canUnlink,
|
||||
managedModpackWarning,
|
||||
isManagedContent,
|
||||
canMutateContent,
|
||||
canUpdateContent,
|
||||
deleteWarning,
|
||||
disableWarning,
|
||||
}
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
warningHeader: {
|
||||
id: 'content.shared-instance.warning-header',
|
||||
defaultMessage: 'This is part of the shared instance',
|
||||
},
|
||||
changeVersionBody: {
|
||||
id: 'content.shared-instance.change-version-body',
|
||||
defaultMessage:
|
||||
'Changing the version only changes your local copy. Future shared instance updates may restore or change it again.',
|
||||
},
|
||||
unlinkBody: {
|
||||
id: 'content.shared-instance.unlink-body',
|
||||
defaultMessage:
|
||||
'Unlinking only changes your local copy. Future shared instance updates may restore or change it again.',
|
||||
},
|
||||
deleteSingleBody: {
|
||||
id: 'content.shared-instance.delete-single-body',
|
||||
defaultMessage:
|
||||
'Deleting it only changes your local copy. Future shared instance updates may restore or change it again.',
|
||||
},
|
||||
deleteBulkBody: {
|
||||
id: 'content.shared-instance.delete-bulk-body',
|
||||
defaultMessage:
|
||||
'Some selected projects are part of the shared instance. Deleting them only changes your local copy, and future shared instance updates may restore or change them again.',
|
||||
},
|
||||
deleteButton: { id: 'content.shared-instance.delete-button', defaultMessage: 'Delete anyway' },
|
||||
deleteManyButton: {
|
||||
id: 'content.shared-instance.delete-many-button',
|
||||
defaultMessage: 'Delete {count, number} projects anyway',
|
||||
},
|
||||
disableSingleBody: {
|
||||
id: 'content.shared-instance.disable-single-body',
|
||||
defaultMessage:
|
||||
'Disabling it only changes your local copy. Future shared instance updates may re-enable, restore, or change it again.',
|
||||
},
|
||||
disableBulkBody: {
|
||||
id: 'content.shared-instance.disable-bulk-body',
|
||||
defaultMessage:
|
||||
'Some selected projects are part of the shared instance. Disabling them only changes your local copy, and future shared instance updates may re-enable, restore, or change them again.',
|
||||
},
|
||||
disableButton: { id: 'content.shared-instance.disable-button', defaultMessage: 'Disable anyway' },
|
||||
disableManyButton: {
|
||||
id: 'content.shared-instance.disable-many-button',
|
||||
defaultMessage: 'Disable {count, number} projects anyway',
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, type MaybeRefOrGetter, onUnmounted, toValue } from 'vue'
|
||||
|
||||
import { toError } from '@/helpers/errors'
|
||||
import { friend_listener } from '@/helpers/events.js'
|
||||
import {
|
||||
acceptCachedFriend,
|
||||
add_friend,
|
||||
createPendingFriend,
|
||||
type FriendCacheUser,
|
||||
friendsQueryKey,
|
||||
type FriendWithUserData,
|
||||
getFriendsWithUserData,
|
||||
getFriendUserId,
|
||||
matchesFriend,
|
||||
remove_friend,
|
||||
removeCachedFriend,
|
||||
upsertCachedFriend,
|
||||
} from '@/helpers/friends'
|
||||
import type { ModrinthCredentials } from '@/helpers/mr_auth'
|
||||
|
||||
type FriendsMutationContext = {
|
||||
queryKey: ReturnType<typeof friendsQueryKey>
|
||||
previousFriends?: FriendWithUserData[]
|
||||
}
|
||||
|
||||
type AddFriendMutationVariables = {
|
||||
userId: string
|
||||
user: FriendCacheUser
|
||||
acceptExisting: boolean
|
||||
}
|
||||
|
||||
type RemoveFriendMutationVariables = {
|
||||
userId: string
|
||||
user: FriendWithUserData
|
||||
}
|
||||
|
||||
export function useFriends(options: {
|
||||
currentUserId: MaybeRefOrGetter<string | null | undefined>
|
||||
getCredentials: () => ModrinthCredentials | null | Promise<ModrinthCredentials | null>
|
||||
enabled?: MaybeRefOrGetter<boolean>
|
||||
onError?: (error: Error) => void
|
||||
}) {
|
||||
const queryClient = useQueryClient()
|
||||
const queryKey = computed(() => friendsQueryKey(toValue(options.currentUserId)))
|
||||
const query = useQuery({
|
||||
queryKey,
|
||||
queryFn: async () => getFriendsWithUserData(await options.getCredentials()),
|
||||
enabled: () => !!toValue(options.currentUserId) && toValue(options.enabled ?? true),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
const friends = computed(() => query.data.value ?? [])
|
||||
|
||||
function restore(context?: FriendsMutationContext) {
|
||||
if (!context) return
|
||||
if (context.previousFriends === undefined) {
|
||||
queryClient.removeQueries({ queryKey: context.queryKey, exact: true })
|
||||
return
|
||||
}
|
||||
queryClient.setQueryData(context.queryKey, context.previousFriends)
|
||||
}
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: ({ userId }: AddFriendMutationVariables) => add_friend(userId),
|
||||
onMutate: async ({ user, acceptExisting }): Promise<FriendsMutationContext> => {
|
||||
const activeQueryKey = queryKey.value
|
||||
await queryClient.cancelQueries({ queryKey: activeQueryKey })
|
||||
const previousFriends = queryClient.getQueryData<FriendWithUserData[]>(activeQueryKey)
|
||||
const currentUserId = toValue(options.currentUserId)
|
||||
queryClient.setQueryData<FriendWithUserData[]>(activeQueryKey, (cachedFriends = []) =>
|
||||
acceptExisting
|
||||
? acceptCachedFriend(cachedFriends, user.id, user.username, currentUserId)
|
||||
: upsertCachedFriend(
|
||||
cachedFriends,
|
||||
createPendingFriend(user, currentUserId),
|
||||
currentUserId,
|
||||
),
|
||||
)
|
||||
return { queryKey: activeQueryKey, previousFriends }
|
||||
},
|
||||
onError: (error, _variables, context) => {
|
||||
restore(context)
|
||||
options.onError?.(toError(error))
|
||||
},
|
||||
onSettled: (_data, _error, _variables, context) => {
|
||||
void queryClient.invalidateQueries({ queryKey: context?.queryKey ?? queryKey.value })
|
||||
},
|
||||
})
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: ({ userId }: RemoveFriendMutationVariables) => remove_friend(userId),
|
||||
onMutate: async ({ user, userId }): Promise<FriendsMutationContext> => {
|
||||
const activeQueryKey = queryKey.value
|
||||
await queryClient.cancelQueries({ queryKey: activeQueryKey })
|
||||
const previousFriends = queryClient.getQueryData<FriendWithUserData[]>(activeQueryKey)
|
||||
queryClient.setQueryData<FriendWithUserData[]>(activeQueryKey, (cachedFriends = []) =>
|
||||
removeCachedFriend(cachedFriends, userId, user.username, toValue(options.currentUserId)),
|
||||
)
|
||||
return { queryKey: activeQueryKey, previousFriends }
|
||||
},
|
||||
onError: (error, _variables, context) => {
|
||||
restore(context)
|
||||
options.onError?.(toError(error))
|
||||
},
|
||||
onSettled: (_data, _error, _variables, context) => {
|
||||
void queryClient.invalidateQueries({ queryKey: context?.queryKey ?? queryKey.value })
|
||||
},
|
||||
})
|
||||
|
||||
function requestFriend(user: FriendCacheUser, acceptExisting = false) {
|
||||
addMutation.mutate({ userId: user.id, user, acceptExisting })
|
||||
}
|
||||
|
||||
function acceptFriend(friend: FriendWithUserData) {
|
||||
const userId = getFriendUserId(friend, toValue(options.currentUserId))
|
||||
requestFriend({ id: userId, username: friend.username, avatarUrl: friend.avatar }, true)
|
||||
}
|
||||
|
||||
function removeFriend(friend: FriendWithUserData) {
|
||||
const userId = getFriendUserId(friend, toValue(options.currentUserId))
|
||||
removeMutation.mutate({ userId, user: friend })
|
||||
}
|
||||
|
||||
function findFriend(id: string, username: string) {
|
||||
return friends.value.find((friend) =>
|
||||
matchesFriend(friend, id, username, toValue(options.currentUserId)),
|
||||
)
|
||||
}
|
||||
|
||||
let unlisten: (() => void) | undefined
|
||||
void friend_listener(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
}).then((listener) => {
|
||||
unlisten = listener
|
||||
})
|
||||
onUnmounted(() => unlisten?.())
|
||||
|
||||
return {
|
||||
query,
|
||||
queryKey,
|
||||
friends,
|
||||
loading: computed(() => !!toValue(options.currentUserId) && query.isLoading.value),
|
||||
requestFriend,
|
||||
acceptFriend,
|
||||
removeFriend,
|
||||
findFriend,
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,8 @@ interface InstanceConsoleEntry {
|
||||
|
||||
const instances = new Map<string, InstanceConsoleEntry>()
|
||||
|
||||
function getOrCreate(profilePathId: string): InstanceConsoleEntry {
|
||||
let entry = instances.get(profilePathId)
|
||||
function getOrCreate(instanceId: string): InstanceConsoleEntry {
|
||||
let entry = instances.get(instanceId)
|
||||
if (entry) return entry
|
||||
|
||||
entry = {
|
||||
@@ -32,25 +32,25 @@ function getOrCreate(profilePathId: string): InstanceConsoleEntry {
|
||||
historicalCache: new Map(),
|
||||
logList: null,
|
||||
}
|
||||
instances.set(profilePathId, entry)
|
||||
instances.set(instanceId, entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
async function hydrate(profilePathId: string): Promise<void> {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
async function hydrate(instanceId: string): Promise<void> {
|
||||
const entry = getOrCreate(instanceId)
|
||||
if (entry.liveConsole.output.value.length > 0) return
|
||||
|
||||
const buffer = await get_live_log_buffer(profilePathId)
|
||||
const buffer = await get_live_log_buffer(instanceId)
|
||||
if (buffer) {
|
||||
entry.liveConsole.addLegacyLog(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
async function getHistoricalLogs(profilePathId: string, instancePath: string): Promise<LogEntry[]> {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
async function getHistoricalLogs(instanceId: string): Promise<LogEntry[]> {
|
||||
const entry = getOrCreate(instanceId)
|
||||
if (entry.logList) return entry.logList
|
||||
|
||||
const logs: LogEntry[] = await get_logs(instancePath, true)
|
||||
const logs: LogEntry[] = await get_logs(instanceId, true)
|
||||
entry.logList = logs
|
||||
|
||||
for (const log of logs) {
|
||||
@@ -62,38 +62,38 @@ async function getHistoricalLogs(profilePathId: string, instancePath: string): P
|
||||
return logs
|
||||
}
|
||||
|
||||
function getHistoricalContent(profilePathId: string, filename: string): string | undefined {
|
||||
return instances.get(profilePathId)?.historicalCache.get(filename)
|
||||
function getHistoricalContent(instanceId: string, filename: string): string | undefined {
|
||||
return instances.get(instanceId)?.historicalCache.get(filename)
|
||||
}
|
||||
|
||||
function invalidate(profilePathId: string): void {
|
||||
const entry = instances.get(profilePathId)
|
||||
function invalidate(instanceId: string): void {
|
||||
const entry = instances.get(instanceId)
|
||||
if (!entry) return
|
||||
entry.historicalCache.clear()
|
||||
entry.logList = null
|
||||
}
|
||||
|
||||
async function clearLive(profilePathId: string): Promise<void> {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
async function clearLive(instanceId: string): Promise<void> {
|
||||
const entry = getOrCreate(instanceId)
|
||||
entry.liveConsole.clear()
|
||||
await clear_log_buffer(profilePathId).catch(() => {})
|
||||
await clear_log_buffer(instanceId).catch(() => {})
|
||||
}
|
||||
|
||||
async function destroy(profilePathId: string): Promise<void> {
|
||||
instances.delete(profilePathId)
|
||||
await clear_log_buffer(profilePathId).catch(() => {})
|
||||
async function destroy(instanceId: string): Promise<void> {
|
||||
instances.delete(instanceId)
|
||||
await clear_log_buffer(instanceId).catch(() => {})
|
||||
}
|
||||
|
||||
export function useInstanceConsole(profilePathId: string) {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
export function useInstanceConsole(instanceId: string) {
|
||||
const entry = getOrCreate(instanceId)
|
||||
return {
|
||||
liveConsole: entry.liveConsole,
|
||||
historicalConsole: entry.historicalConsole,
|
||||
hydrate: () => hydrate(profilePathId),
|
||||
getHistoricalLogs: (instancePath: string) => getHistoricalLogs(profilePathId, instancePath),
|
||||
getHistoricalContent: (filename: string) => getHistoricalContent(profilePathId, filename),
|
||||
invalidate: () => invalidate(profilePathId),
|
||||
clearLive: () => clearLive(profilePathId),
|
||||
destroy: () => destroy(profilePathId),
|
||||
hydrate: () => hydrate(instanceId),
|
||||
getHistoricalLogs: () => getHistoricalLogs(instanceId),
|
||||
getHistoricalContent: (filename: string) => getHistoricalContent(instanceId, filename),
|
||||
invalidate: () => invalidate(instanceId),
|
||||
clearLive: () => clearLive(instanceId),
|
||||
destroy: () => destroy(instanceId),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, type Ref } from 'vue'
|
||||
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
|
||||
export function useUserQuery(userId: Ref<string | null | undefined>) {
|
||||
return useQuery({
|
||||
queryKey: computed(() => ['user', userId.value]),
|
||||
queryFn: async ({ queryKey }) => {
|
||||
const id = queryKey[1]
|
||||
if (typeof id !== 'string') return null
|
||||
return await get_user(id, 'bypass').catch(() => null)
|
||||
},
|
||||
enabled: () => !!userId.value,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function toError(error: unknown) {
|
||||
if (error instanceof Error) return error
|
||||
if (typeof error === 'string') return new Error(error)
|
||||
if (error && typeof error === 'object') {
|
||||
const record = error as Record<string, unknown>
|
||||
const message = record.message ?? record.error
|
||||
if (typeof message === 'string') return new Error(message)
|
||||
return new Error(JSON.stringify(error))
|
||||
}
|
||||
return new Error(String(error))
|
||||
}
|
||||
@@ -31,8 +31,8 @@ import { listen } from '@tauri-apps/api/event'
|
||||
pack_name: name of the pack
|
||||
pack_id, optional, the id of the modpack
|
||||
pack_version, optional, the version of the modpack
|
||||
profile_name: name of the profile
|
||||
profile_uuid: unique identification of the profile
|
||||
instance_name: name of the instance
|
||||
instance_id: unique identification of the instance
|
||||
|
||||
}
|
||||
loader_uuid: unique identification of the loading bar
|
||||
@@ -57,18 +57,32 @@ export async function process_listener(callback) {
|
||||
return await listen('process', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'profile' event
|
||||
/// Payload for the 'instance' event
|
||||
/*
|
||||
ProfilePayload {
|
||||
uuid: unique identification of the process in the state (currently identified by path, but that will change)
|
||||
name: name of the profile
|
||||
profile_path: relative path toprofile_listener profile (used for path identification)
|
||||
path: path to profile (used for opening the profile in the OS file explorer)
|
||||
InstancePayload {
|
||||
instance_id: unique identification of the instance
|
||||
event: event type ("Created", "Added", "Edited", "Removed")
|
||||
}
|
||||
*/
|
||||
export async function profile_listener(callback) {
|
||||
return await listen('profile', (event) => callback(event.payload))
|
||||
export async function instance_listener(callback) {
|
||||
return await listen('instance', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'instance_bulk_update_progress' event
|
||||
/*
|
||||
InstanceBulkUpdateProgress {
|
||||
instanceId: string
|
||||
stage: "resolving_versions" | "downloading" | "finishing"
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
*/
|
||||
export async function instance_bulk_update_progress_listener(callback) {
|
||||
return await listen('instance_bulk_update_progress', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function install_job_listener(callback) {
|
||||
return await listen('install_job', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'command' event
|
||||
@@ -105,7 +119,7 @@ export async function notification_listener(callback) {
|
||||
/// Payload for the 'log' event
|
||||
/*
|
||||
LogPayload {
|
||||
profile_path_id: string,
|
||||
instance_id: string,
|
||||
type: "log4j" | "legacy",
|
||||
// log4j fields (when type === "log4j"):
|
||||
timestamp_millis?: number,
|
||||
|
||||
@@ -6,9 +6,11 @@ import dayjs from 'dayjs'
|
||||
import { get_user_many } from '@/helpers/cache'
|
||||
import type { ModrinthCredentials } from '@/helpers/mr_auth'
|
||||
|
||||
export const friendsQueryKey = (userId?: string | null) => ['friends', userId ?? null] as const
|
||||
|
||||
export type UserStatus = {
|
||||
user_id: string
|
||||
profile_name: string | null
|
||||
instance_name: string | null
|
||||
last_update: string
|
||||
}
|
||||
|
||||
@@ -46,6 +48,111 @@ export type FriendWithUserData = {
|
||||
online: boolean
|
||||
avatar: string
|
||||
}
|
||||
|
||||
export type FriendCacheUser = {
|
||||
id: string
|
||||
username: string
|
||||
avatarUrl?: string | null
|
||||
}
|
||||
|
||||
export async function getFriendsWithUserData(
|
||||
credentials: ModrinthCredentials | null,
|
||||
): Promise<FriendWithUserData[]> {
|
||||
if (!credentials) return []
|
||||
|
||||
const friendsList = await friends()
|
||||
return await transformFriends(friendsList, credentials)
|
||||
}
|
||||
|
||||
export function createPendingFriend(
|
||||
user: FriendCacheUser,
|
||||
currentUserId?: string | null,
|
||||
): FriendWithUserData {
|
||||
return {
|
||||
id: user.id,
|
||||
friend_id: currentUserId ?? null,
|
||||
status: null,
|
||||
last_updated: null,
|
||||
created: dayjs(),
|
||||
username: user.username,
|
||||
accepted: false,
|
||||
online: false,
|
||||
avatar: user.avatarUrl ?? '',
|
||||
}
|
||||
}
|
||||
|
||||
export function getFriendUserId(
|
||||
friend: Pick<FriendWithUserData, 'id' | 'friend_id'>,
|
||||
currentUserId?: string | null,
|
||||
) {
|
||||
return friend.id === currentUserId && friend.friend_id ? friend.friend_id : friend.id
|
||||
}
|
||||
|
||||
export function matchesFriend(
|
||||
friend: FriendWithUserData,
|
||||
id: string,
|
||||
username: string,
|
||||
currentUserId?: string | null,
|
||||
) {
|
||||
const friendId = getFriendUserId(friend, currentUserId)
|
||||
return (
|
||||
normalizeFriendKey(friendId) === normalizeFriendKey(id) ||
|
||||
normalizeFriendKey(friend.username) === normalizeFriendKey(username)
|
||||
)
|
||||
}
|
||||
|
||||
export function upsertCachedFriend(
|
||||
friends: FriendWithUserData[],
|
||||
friend: FriendWithUserData,
|
||||
currentUserId?: string | null,
|
||||
) {
|
||||
const existingFriend = friends.find((cachedFriend) =>
|
||||
matchesFriend(cachedFriend, friend.id, friend.username, currentUserId),
|
||||
)
|
||||
|
||||
if (!existingFriend) return [friend, ...friends]
|
||||
|
||||
return friends.map((cachedFriend) =>
|
||||
matchesFriend(cachedFriend, friend.id, friend.username, currentUserId)
|
||||
? {
|
||||
...cachedFriend,
|
||||
...friend,
|
||||
id: cachedFriend.id,
|
||||
friend_id: cachedFriend.friend_id,
|
||||
}
|
||||
: cachedFriend,
|
||||
)
|
||||
}
|
||||
|
||||
export function acceptCachedFriend(
|
||||
friends: FriendWithUserData[],
|
||||
id: string,
|
||||
username: string,
|
||||
currentUserId?: string | null,
|
||||
) {
|
||||
return friends.map((friend) =>
|
||||
matchesFriend(friend, id, username, currentUserId)
|
||||
? {
|
||||
...friend,
|
||||
accepted: true,
|
||||
}
|
||||
: friend,
|
||||
)
|
||||
}
|
||||
|
||||
export function removeCachedFriend(
|
||||
friends: FriendWithUserData[],
|
||||
id: string,
|
||||
username: string,
|
||||
currentUserId?: string | null,
|
||||
) {
|
||||
return friends.filter((friend) => !matchesFriend(friend, id, username, currentUserId))
|
||||
}
|
||||
|
||||
export function normalizeFriendKey(value: string) {
|
||||
return value.trim().toLowerCase()
|
||||
}
|
||||
|
||||
export async function transformFriends(
|
||||
friends: UserFriend[],
|
||||
credentials: ModrinthCredentials | null,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import { create } from './profile'
|
||||
import { install_import_instance } from './install'
|
||||
|
||||
/*
|
||||
API for importing instances from other launchers
|
||||
@@ -32,19 +32,8 @@ export async function get_importable_instances(launcherType, basePath) {
|
||||
}
|
||||
|
||||
/// Import an instance from a launcher type and base path
|
||||
/// eg: import_instance("profile-name-to-go-to", "MultiMC", "C:/MultiMC", "Instance 1")
|
||||
export async function import_instance(launcherType, basePath, instanceFolder) {
|
||||
// create a basic, empty instance (most properties will be filled in by the import process)
|
||||
// We do NOT watch the fs for changes to avoid duplicate events during installation
|
||||
// fs watching will be enabled once the instance is imported
|
||||
const profilePath = await create(instanceFolder, '1.19.4', 'vanilla', 'latest', null, true)
|
||||
|
||||
return await invoke('plugin:import|import_instance', {
|
||||
profilePath,
|
||||
launcherType,
|
||||
basePath,
|
||||
instanceFolder,
|
||||
})
|
||||
return await install_import_instance(launcherType, basePath, instanceFolder)
|
||||
}
|
||||
|
||||
/// Checks if this instance is valid for importing, given a certain launcher type
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import { install_job_listener } from './events'
|
||||
import type { InstanceLink, InstanceLoader } from './types'
|
||||
|
||||
export interface PackLocationVersionId {
|
||||
type: 'fromVersionId'
|
||||
project_id: string
|
||||
version_id: string
|
||||
title: string
|
||||
icon_url?: string | null
|
||||
}
|
||||
|
||||
export interface PackLocationFile {
|
||||
type: 'fromFile'
|
||||
path: string
|
||||
}
|
||||
|
||||
export type CreatePackLocation = PackLocationVersionId | PackLocationFile
|
||||
|
||||
export interface InstallModpackPreview {
|
||||
name: string
|
||||
gameVersion: string
|
||||
modloader: InstanceLoader
|
||||
loaderVersion: string | null
|
||||
icon?: string | null
|
||||
iconUrl?: string | null
|
||||
link?: InstanceLink | null
|
||||
unknownFile: boolean
|
||||
}
|
||||
|
||||
export interface InstallCreateInstanceRequest {
|
||||
name: string
|
||||
gameVersion: string
|
||||
loader: InstanceLoader
|
||||
loaderVersion: string | null
|
||||
iconPath: string | null
|
||||
link?: InstanceLink | null
|
||||
}
|
||||
|
||||
export interface InstallPostInstallEdit {
|
||||
name?: string | null
|
||||
iconPath?: string | null
|
||||
link?: InstanceLink | null
|
||||
}
|
||||
|
||||
export interface SharedInstanceInstallPreview {
|
||||
name: string
|
||||
iconUrl?: string | null
|
||||
gameVersion: string
|
||||
loader: InstanceLoader
|
||||
modCount: number
|
||||
externalFileCount: number
|
||||
modpackVersionId?: string | null
|
||||
contentVersionIds: string[]
|
||||
externalFiles: SharedInstanceExternalFilePreview[]
|
||||
}
|
||||
|
||||
export interface SharedInstanceInviteInstallPreview {
|
||||
sharedInstanceId: string
|
||||
managerId?: string | null
|
||||
serverManagerName?: string | null
|
||||
serverManagerIconUrl?: string | null
|
||||
instanceIconUrl?: string | null
|
||||
preview: SharedInstanceInstallPreview
|
||||
}
|
||||
|
||||
export interface SharedInstanceExternalFilePreview {
|
||||
fileName: string
|
||||
fileType: string
|
||||
}
|
||||
|
||||
export interface SharedInstanceUpdatePreview {
|
||||
sharedInstanceId: string
|
||||
currentVersion?: number | null
|
||||
latestVersion: number
|
||||
updateAvailable: boolean
|
||||
diffs: SharedInstanceUpdateDiff[]
|
||||
}
|
||||
|
||||
export interface SharedInstanceUpdateDiff {
|
||||
type:
|
||||
| 'added'
|
||||
| 'removed'
|
||||
| 'updated'
|
||||
| 'modpack_linked'
|
||||
| 'modpack_updated'
|
||||
| 'modpack_unlinked'
|
||||
| 'game_version_updated'
|
||||
| 'loader_updated'
|
||||
projectId?: string | null
|
||||
projectName?: string | null
|
||||
fileName?: string | null
|
||||
currentVersionName?: string | null
|
||||
newVersionName?: string | null
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
|
||||
|
||||
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked'
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
export function isSharedInstanceUnavailableError(error: unknown) {
|
||||
return getSharedInstanceUnavailableReason(error) !== null
|
||||
}
|
||||
|
||||
export function getSharedInstanceUnavailableReason(
|
||||
error: unknown,
|
||||
): SharedInstanceUnavailableReason | null {
|
||||
if (!isRecord(error) || error.code !== SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE) return null
|
||||
return error.reason === 'deleted' || error.reason === 'access_revoked' ? error.reason : null
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'string') return error
|
||||
if (error instanceof Error) return error.message || 'Unknown error'
|
||||
if (isRecord(error) && typeof error.message === 'string') return error.message
|
||||
return 'Unknown error'
|
||||
}
|
||||
|
||||
export type InstallJobStatus =
|
||||
| 'queued'
|
||||
| 'running'
|
||||
| 'succeeded'
|
||||
| 'failed'
|
||||
| 'interrupted'
|
||||
| 'canceled'
|
||||
|
||||
export type InstallPhaseId =
|
||||
| 'preparing_instance'
|
||||
| 'resolving_pack'
|
||||
| 'downloading_pack_file'
|
||||
| 'reading_pack_manifest'
|
||||
| 'downloading_content'
|
||||
| 'extracting_overrides'
|
||||
| 'resolving_minecraft'
|
||||
| 'resolving_loader'
|
||||
| 'preparing_java'
|
||||
| 'downloading_minecraft'
|
||||
| 'running_loader_processors'
|
||||
| 'finalizing'
|
||||
| 'rolling_back'
|
||||
|
||||
export interface InstallProgress {
|
||||
current: number
|
||||
total: number
|
||||
secondary?: InstallProgressSecondary | null
|
||||
}
|
||||
|
||||
export interface InstallProgressSecondary {
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type InstallJavaStep =
|
||||
| 'resolving'
|
||||
| 'fetching_metadata'
|
||||
| 'downloading'
|
||||
| 'extracting'
|
||||
| 'validating'
|
||||
|
||||
export interface InstallErrorView {
|
||||
code: string
|
||||
phase?: InstallPhaseId | null
|
||||
message: string
|
||||
reason?: SharedInstanceUnavailableReason | null
|
||||
api?: {
|
||||
error: string
|
||||
status?: number | null
|
||||
method?: string | null
|
||||
url?: string | null
|
||||
route?: string | null
|
||||
} | null
|
||||
context?: {
|
||||
operation: string
|
||||
source_path?: string | null
|
||||
target_path?: string | null
|
||||
file_path?: string | null
|
||||
entry_path?: string | null
|
||||
urls?: string[]
|
||||
expected_hash?: string | null
|
||||
expected_size?: number | null
|
||||
project_id?: string | null
|
||||
version_id?: string | null
|
||||
minecraft_version?: string | null
|
||||
loader?: string | null
|
||||
java_version?: number | null
|
||||
os?: string | null
|
||||
arch?: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface InstallJobSnapshot {
|
||||
job_id: string
|
||||
instance_id?: string | null
|
||||
kind:
|
||||
| 'create_instance'
|
||||
| 'create_modpack_instance'
|
||||
| 'create_shared_instance'
|
||||
| 'update_shared_instance'
|
||||
| 'import_instance'
|
||||
| 'duplicate_instance'
|
||||
| 'install_existing_instance'
|
||||
| 'install_pack_to_existing_instance'
|
||||
status: InstallJobStatus
|
||||
target:
|
||||
| { type: 'new_instance'; instance_id?: string | null }
|
||||
| { type: 'existing_instance'; instance_id: string }
|
||||
phase: InstallPhaseId
|
||||
progress?: InstallProgress | null
|
||||
details:
|
||||
| { type: 'empty' }
|
||||
| { type: 'instance'; name: string }
|
||||
| { type: 'minecraft'; game_version: string; loader: InstanceLoader }
|
||||
| { type: 'java'; major_version: number; step: InstallJavaStep }
|
||||
| {
|
||||
type: 'modpack'
|
||||
project_id?: string | null
|
||||
version_id?: string | null
|
||||
title?: string | null
|
||||
}
|
||||
| { type: 'import'; launcher_type: string; instance_folder: string }
|
||||
display?: { title: string; icon?: string | null } | null
|
||||
error?: InstallErrorView | null
|
||||
rollback_error?: InstallErrorView | null
|
||||
created: string
|
||||
modified: string
|
||||
finished?: string | null
|
||||
}
|
||||
|
||||
export async function install_get_modpack_preview(location: CreatePackLocation) {
|
||||
return await invoke<InstallModpackPreview>('plugin:install|install_get_modpack_preview', {
|
||||
location,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_create_instance(request: InstallCreateInstanceRequest) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_create_instance', { request })
|
||||
}
|
||||
|
||||
export async function install_create_modpack_instance(
|
||||
location: CreatePackLocation,
|
||||
postInstallEdit?: InstallPostInstallEdit | null,
|
||||
) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_create_modpack_instance', {
|
||||
location,
|
||||
postInstallEdit,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_get_shared_instance_preview(sharedInstanceId: string, name: string) {
|
||||
return await invoke<SharedInstanceInstallPreview>(
|
||||
'plugin:install|install_get_shared_instance_preview',
|
||||
{
|
||||
sharedInstanceId,
|
||||
name,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function install_accept_shared_instance_invite(inviteId: string) {
|
||||
return await invoke<SharedInstanceInviteInstallPreview>(
|
||||
'plugin:install|install_accept_shared_instance_invite',
|
||||
{
|
||||
inviteId,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function install_get_shared_instance_update_preview(instanceId: string) {
|
||||
return await invoke<SharedInstanceUpdatePreview | null>(
|
||||
'plugin:install|install_get_shared_instance_update_preview',
|
||||
{
|
||||
instanceId,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function install_shared_instance(
|
||||
sharedInstanceId: string,
|
||||
name: string,
|
||||
managerId?: string | null,
|
||||
serverManagerName?: string | null,
|
||||
serverManagerIconUrl?: string | null,
|
||||
instanceIconUrl?: string | null,
|
||||
) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_shared_instance', {
|
||||
sharedInstanceId,
|
||||
name,
|
||||
managerId,
|
||||
serverManagerName,
|
||||
serverManagerIconUrl,
|
||||
instanceIconUrl,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_update_shared_instance(instanceId: string) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_update_shared_instance', {
|
||||
instanceId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_import_instance(
|
||||
launcherType: string,
|
||||
basePath: string,
|
||||
instanceFolder: string,
|
||||
) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_import_instance', {
|
||||
launcherType,
|
||||
basePath,
|
||||
instanceFolder,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_duplicate_instance(sourceInstanceId: string) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_duplicate_instance', {
|
||||
sourceInstanceId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_existing_instance(instanceId: string, force: boolean) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_existing_instance', {
|
||||
instanceId,
|
||||
force,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_pack_to_existing_instance(
|
||||
instanceId: string,
|
||||
location: CreatePackLocation,
|
||||
postInstallEdit?: InstallPostInstallEdit | null,
|
||||
) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_pack_to_existing_instance', {
|
||||
instanceId,
|
||||
location,
|
||||
postInstallEdit,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_job_list(includeFinished: boolean) {
|
||||
return await invoke<InstallJobSnapshot[]>('plugin:install|install_job_list', { includeFinished })
|
||||
}
|
||||
|
||||
export async function install_job_get(jobId: string) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_job_get', { jobId })
|
||||
}
|
||||
|
||||
export async function install_job_retry(jobId: string) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_job_retry', { jobId })
|
||||
}
|
||||
|
||||
export async function install_job_cancel(jobId: string) {
|
||||
return await invoke<InstallJobSnapshot>('plugin:install|install_job_cancel', { jobId })
|
||||
}
|
||||
|
||||
export async function install_job_dismiss(jobId: string) {
|
||||
return await invoke<void>('plugin:install|install_job_dismiss', { jobId })
|
||||
}
|
||||
|
||||
export async function install_job_support_details(jobId: string) {
|
||||
return await invoke<string>('plugin:install|install_job_support_details', { jobId })
|
||||
}
|
||||
|
||||
export function installJobInstanceId(job: InstallJobSnapshot): string | null {
|
||||
return job.instance_id ?? job.target.instance_id ?? null
|
||||
}
|
||||
|
||||
export function isInstallJobFinished(status: InstallJobStatus) {
|
||||
return (
|
||||
status === 'succeeded' ||
|
||||
status === 'failed' ||
|
||||
status === 'interrupted' ||
|
||||
status === 'canceled'
|
||||
)
|
||||
}
|
||||
|
||||
function settleInstallJob(job: InstallJobSnapshot) {
|
||||
if (job.status === 'succeeded') return job
|
||||
|
||||
if (job.error) throw job.error
|
||||
throw new Error(`Install job ${job.job_id} ${job.status}`)
|
||||
}
|
||||
|
||||
export async function wait_for_install_job(jobId: string) {
|
||||
const current = await install_job_get(jobId)
|
||||
if (isInstallJobFinished(current.status)) return settleInstallJob(current)
|
||||
|
||||
return await new Promise<InstallJobSnapshot>((resolve, reject) => {
|
||||
let finished = false
|
||||
let unlisten: (() => void) | null = null
|
||||
|
||||
const cleanup = () => {
|
||||
if (unlisten) {
|
||||
unlisten()
|
||||
unlisten = null
|
||||
}
|
||||
}
|
||||
|
||||
const resolveJob = (job: InstallJobSnapshot) => {
|
||||
if (finished || job.job_id !== jobId || !isInstallJobFinished(job.status)) return
|
||||
|
||||
finished = true
|
||||
cleanup()
|
||||
|
||||
try {
|
||||
resolve(settleInstallJob(job))
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
const rejectWait = (err: unknown) => {
|
||||
if (finished) return
|
||||
finished = true
|
||||
cleanup()
|
||||
reject(err)
|
||||
}
|
||||
|
||||
install_job_listener(resolveJob)
|
||||
.then((listener) => {
|
||||
if (finished) {
|
||||
listener()
|
||||
return
|
||||
}
|
||||
|
||||
unlisten = listener
|
||||
install_job_get(jobId).then(resolveJob).catch(rejectWait)
|
||||
})
|
||||
.catch(rejectWait)
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
get_content_items,
|
||||
get_linked_modpack_info,
|
||||
type LinkedModpackInfo,
|
||||
} from '@/helpers/profile'
|
||||
} from '@/helpers/instance'
|
||||
import { get_categories } from '@/helpers/tags.js'
|
||||
import type { CacheBehaviour } from '@/helpers/types'
|
||||
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { ContentItem, ContentOwner } from '@modrinth/ui'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import type { InstallJobSnapshot, SharedInstanceUpdateDiff } from './install'
|
||||
import type {
|
||||
CacheBehaviour,
|
||||
ContentFile,
|
||||
ContentFileProjectType,
|
||||
GameInstance,
|
||||
InstanceLoader,
|
||||
SharedInstanceAttachment,
|
||||
} from './types'
|
||||
|
||||
export async function remove(instanceId: string): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_remove', { instanceId })
|
||||
}
|
||||
|
||||
export async function get(instanceId: string): Promise<GameInstance | null> {
|
||||
return await invoke('plugin:instance|instance_get', { instanceId })
|
||||
}
|
||||
|
||||
export async function get_many(instanceIds: string[]): Promise<GameInstance[]> {
|
||||
return await invoke('plugin:instance|instance_get_many', { instanceIds })
|
||||
}
|
||||
|
||||
export async function get_projects(
|
||||
instanceId: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<Record<string, ContentFile>> {
|
||||
return await invoke('plugin:instance|instance_get_projects', { instanceId, cacheBehaviour })
|
||||
}
|
||||
|
||||
export async function get_installed_project_ids(instanceId: string): Promise<string[]> {
|
||||
return await invoke('plugin:instance|instance_get_installed_project_ids', { instanceId })
|
||||
}
|
||||
|
||||
export type InstanceInstallTarget = {
|
||||
game_version: string
|
||||
loader: string
|
||||
}
|
||||
|
||||
export type InstanceInstallCandidate = {
|
||||
id: string
|
||||
name: string
|
||||
icon_path?: string | null
|
||||
game_version: string
|
||||
loader: InstanceLoader
|
||||
installed: boolean
|
||||
compatible: boolean
|
||||
}
|
||||
|
||||
export async function get_install_candidates(
|
||||
projectId: string,
|
||||
projectType: string,
|
||||
targets: InstanceInstallTarget[],
|
||||
): Promise<InstanceInstallCandidate[]> {
|
||||
return await invoke('plugin:instance|instance_get_install_candidates', {
|
||||
projectId,
|
||||
projectType,
|
||||
targets,
|
||||
})
|
||||
}
|
||||
|
||||
// Get content items with rich metadata for an instance
|
||||
// Returns content items filtered to exclude modpack files (if linked),
|
||||
// sorted alphabetically by project name
|
||||
export async function get_content_items(
|
||||
instanceId: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<ContentItem[]> {
|
||||
return await invoke('plugin:instance|instance_get_content_items', { instanceId, cacheBehaviour })
|
||||
}
|
||||
|
||||
// Linked modpack info returned from backend
|
||||
export interface LinkedModpackInfo {
|
||||
project: Labrinth.Projects.v2.Project
|
||||
version: Labrinth.Versions.v2.Version
|
||||
owner: ContentOwner | null
|
||||
has_update: boolean
|
||||
update_version_id: string | null
|
||||
update_version: Labrinth.Versions.v2.Version | null
|
||||
}
|
||||
|
||||
// Get linked modpack info for an instance
|
||||
// Returns project, version, and owner information for the linked modpack,
|
||||
// or null if the instance is not linked to a modpack
|
||||
export async function get_linked_modpack_info(
|
||||
instanceId: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<LinkedModpackInfo | null> {
|
||||
return await invoke('plugin:instance|instance_get_linked_modpack_info', {
|
||||
instanceId,
|
||||
cacheBehaviour,
|
||||
})
|
||||
}
|
||||
|
||||
// Get content items that are part of the linked modpack
|
||||
// Returns the modpack's dependencies as ContentItem list
|
||||
// Returns empty array if the instance is not linked to a modpack
|
||||
export async function get_linked_modpack_content(
|
||||
instanceId: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<ContentItem[]> {
|
||||
return await invoke('plugin:instance|instance_get_linked_modpack_content', {
|
||||
instanceId,
|
||||
cacheBehaviour,
|
||||
})
|
||||
}
|
||||
|
||||
// Convert a list of dependencies into ContentItems with rich metadata
|
||||
export async function get_dependencies_as_content_items(
|
||||
dependencies: Labrinth.Versions.v3.Dependency[],
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<ContentItem[]> {
|
||||
return await invoke('plugin:instance|instance_get_dependencies_as_content_items', {
|
||||
dependencies,
|
||||
cacheBehaviour,
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_full_path(instanceId: string): Promise<string> {
|
||||
return await invoke('plugin:instance|instance_get_full_path', { instanceId })
|
||||
}
|
||||
|
||||
export async function get_mod_full_path(instanceId: string, projectPath: string): Promise<string> {
|
||||
return await invoke('plugin:instance|instance_get_mod_full_path', { instanceId, projectPath })
|
||||
}
|
||||
|
||||
export interface JavaVersion {
|
||||
parsed_version: number
|
||||
version: string
|
||||
architecture: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export async function get_optimal_jre_key(instanceId: string): Promise<JavaVersion | null> {
|
||||
return await invoke('plugin:instance|instance_get_optimal_jre_key', { instanceId })
|
||||
}
|
||||
|
||||
export async function list(): Promise<GameInstance[]> {
|
||||
return await invoke('plugin:instance|instance_list')
|
||||
}
|
||||
|
||||
export async function check_installed(instanceId: string, projectId: string): Promise<boolean> {
|
||||
return await invoke('plugin:instance|instance_check_installed', { instanceId, projectId })
|
||||
}
|
||||
|
||||
export async function update_all(instanceId: string): Promise<Record<string, string>> {
|
||||
return await invoke('plugin:instance|instance_update_all', { instanceId })
|
||||
}
|
||||
|
||||
// Updates a specified project
|
||||
export async function update_project(instanceId: string, projectPath: string): Promise<string> {
|
||||
return await invoke('plugin:instance|instance_update_project', { instanceId, projectPath })
|
||||
}
|
||||
|
||||
// Add a project to an instance from a version
|
||||
// Returns a path to the new project file
|
||||
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
|
||||
|
||||
export interface ResolutionPreferences {
|
||||
game_versions?: string[]
|
||||
loaders?: string[]
|
||||
}
|
||||
|
||||
export interface ResolveContentRequest {
|
||||
project_id: string
|
||||
version_id?: string | null
|
||||
content_type: Labrinth.Content.v3.ContentType
|
||||
selected?: ResolutionPreferences
|
||||
}
|
||||
|
||||
export interface ResolvedContent {
|
||||
project_id: string
|
||||
version_id: string
|
||||
dependent_on_version_id?: string | null
|
||||
}
|
||||
|
||||
export interface ResolveContentPlan {
|
||||
primary: ResolvedContent
|
||||
dependencies: ResolvedContent[]
|
||||
skipped: Array<{
|
||||
project_id: string
|
||||
version_id?: string | null
|
||||
dependent_on_version_id?: string | null
|
||||
reason: string
|
||||
}>
|
||||
}
|
||||
|
||||
export async function add_project_from_version(
|
||||
instanceId: string,
|
||||
versionId: string,
|
||||
reason: DownloadReason,
|
||||
dependentOnVersionId?: string,
|
||||
): Promise<string> {
|
||||
return await invoke('plugin:instance|instance_add_project_from_version', {
|
||||
instanceId,
|
||||
versionId,
|
||||
reason,
|
||||
dependentOnVersionId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function install_project_with_dependencies(
|
||||
instanceId: string,
|
||||
request: ResolveContentRequest,
|
||||
): Promise<ResolveContentPlan> {
|
||||
return await invoke('plugin:instance|instance_install_project_with_dependencies', {
|
||||
instanceId,
|
||||
request,
|
||||
})
|
||||
}
|
||||
|
||||
export async function switch_project_version_with_dependencies(
|
||||
instanceId: string,
|
||||
projectPath: string,
|
||||
versionId: string,
|
||||
): Promise<string> {
|
||||
return await invoke('plugin:instance|instance_switch_project_version_with_dependencies', {
|
||||
instanceId,
|
||||
projectPath,
|
||||
versionId,
|
||||
})
|
||||
}
|
||||
|
||||
// Add a project to an instance from a path + project_type
|
||||
// Returns a path to the new project file
|
||||
export async function add_project_from_path(
|
||||
instanceId: string,
|
||||
projectPath: string,
|
||||
projectType?: ContentFileProjectType,
|
||||
): Promise<string> {
|
||||
return await invoke('plugin:instance|instance_add_project_from_path', {
|
||||
instanceId,
|
||||
projectPath,
|
||||
projectType,
|
||||
})
|
||||
}
|
||||
|
||||
// Toggle disabling a project
|
||||
export async function toggle_disable_project(
|
||||
instanceId: string,
|
||||
projectPath: string,
|
||||
desiredEnabled?: boolean,
|
||||
): Promise<string> {
|
||||
return await invoke('plugin:instance|instance_toggle_disable_project', {
|
||||
instanceId,
|
||||
projectPath,
|
||||
desiredEnabled,
|
||||
})
|
||||
}
|
||||
|
||||
// Remove a project
|
||||
export async function remove_project(instanceId: string, projectPath: string): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_remove_project', { instanceId, projectPath })
|
||||
}
|
||||
|
||||
// Update a managed Modrinth instance to a specific version
|
||||
export async function update_managed_modrinth_version(
|
||||
instanceId: string,
|
||||
versionId: string,
|
||||
): Promise<InstallJobSnapshot> {
|
||||
return await invoke('plugin:instance|instance_update_managed_modrinth_version', {
|
||||
instanceId,
|
||||
versionId,
|
||||
})
|
||||
}
|
||||
|
||||
// Repair a managed Modrinth instance
|
||||
export async function update_repair_modrinth(instanceId: string): Promise<InstallJobSnapshot> {
|
||||
return await invoke('plugin:instance|instance_repair_managed_modrinth', { instanceId })
|
||||
}
|
||||
|
||||
// Export an instance to .mrpack
|
||||
// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
|
||||
// Version id is optional (ie: 1.1.5)
|
||||
export async function export_instance_mrpack(
|
||||
instanceId: string,
|
||||
exportLocation: string,
|
||||
includedOverrides: string[],
|
||||
versionId?: string,
|
||||
description?: string,
|
||||
name?: string,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_export_mrpack', {
|
||||
instanceId,
|
||||
exportLocation,
|
||||
includedOverrides,
|
||||
versionId,
|
||||
description,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
// Given a folder path, populate an array of all the subfolders
|
||||
// Intended to be used for finding potential override folders
|
||||
// profile
|
||||
// -- mods
|
||||
// -- resourcepacks
|
||||
// -- file1
|
||||
// => [mods, resourcepacks]
|
||||
// allows selection for 'included_overrides' in export_instance_mrpack
|
||||
export async function get_pack_export_candidates(instanceId: string): Promise<string[]> {
|
||||
return await invoke('plugin:instance|instance_get_pack_export_candidates', { instanceId })
|
||||
}
|
||||
|
||||
// Run Minecraft using an instance
|
||||
// Returns PID of child
|
||||
export async function run(
|
||||
instanceId: string,
|
||||
serverAddress: string | null = null,
|
||||
): Promise<unknown> {
|
||||
return await invoke('plugin:instance|instance_run', { instanceId, serverAddress })
|
||||
}
|
||||
|
||||
export async function kill(instanceId: string): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_kill', { instanceId })
|
||||
}
|
||||
|
||||
// Edits an instance
|
||||
export async function edit(instanceId: string, editInstance: Partial<GameInstance>): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_edit', { instanceId, editInstance })
|
||||
}
|
||||
|
||||
// Edits an instance's icon
|
||||
export async function edit_icon(instanceId: string, iconPath: string | null): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_edit_icon', { instanceId, iconPath })
|
||||
}
|
||||
|
||||
export type SharedInstanceUsers = {
|
||||
user_ids: string[]
|
||||
users: SharedInstanceUser[]
|
||||
tokens: number
|
||||
}
|
||||
|
||||
export type SharedInstanceJoinType = 'owner' | 'invite' | 'link'
|
||||
|
||||
export type SharedInstanceUser = {
|
||||
id: string
|
||||
joined_at?: string | null
|
||||
join_type: SharedInstanceJoinType
|
||||
last_played?: string | null
|
||||
}
|
||||
|
||||
export interface SharedInstancePublishPreview {
|
||||
sharedInstanceId: string
|
||||
latestVersion: number
|
||||
diffs: SharedInstanceUpdateDiff[]
|
||||
}
|
||||
|
||||
export interface SharedInstanceInviteLink {
|
||||
inviteId: string
|
||||
expiresAt: string
|
||||
maxUses: number
|
||||
}
|
||||
|
||||
export async function get_shared_instance_users(instanceId: string): Promise<SharedInstanceUsers> {
|
||||
return await invoke('plugin:instance|instance_share_get_users', { instanceId })
|
||||
}
|
||||
|
||||
export async function invite_shared_instance_users(
|
||||
instanceId: string,
|
||||
userIds: string[],
|
||||
): Promise<SharedInstanceUsers> {
|
||||
return await invoke('plugin:instance|instance_share_invite_users', { instanceId, userIds })
|
||||
}
|
||||
|
||||
export async function create_shared_instance_invite_link(
|
||||
instanceId: string,
|
||||
options: {
|
||||
maxAgeSeconds?: number
|
||||
maxUses?: number
|
||||
replaceInviteId?: string
|
||||
} = {},
|
||||
): Promise<SharedInstanceInviteLink> {
|
||||
return await invoke('plugin:instance|instance_share_create_invite_link', {
|
||||
instanceId,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export async function remove_shared_instance_users(
|
||||
instanceId: string,
|
||||
userIds: string[],
|
||||
): Promise<SharedInstanceUsers> {
|
||||
return await invoke('plugin:instance|instance_share_remove_users', { instanceId, userIds })
|
||||
}
|
||||
|
||||
export async function get_shared_instance_publish_preview(
|
||||
instanceId: string,
|
||||
): Promise<SharedInstancePublishPreview | null> {
|
||||
return await invoke('plugin:instance|instance_share_get_publish_preview', { instanceId })
|
||||
}
|
||||
|
||||
export async function publish_shared_instance(
|
||||
instanceId: string,
|
||||
): Promise<SharedInstanceAttachment> {
|
||||
return await invoke('plugin:instance|instance_share_publish', { instanceId })
|
||||
}
|
||||
|
||||
export async function unlink_shared_instance(instanceId: string): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_share_unlink', { instanceId })
|
||||
}
|
||||
|
||||
export async function unpublish_shared_instance(instanceId: string): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_share_unpublish', { instanceId })
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
@@ -14,41 +14,41 @@ pub struct Logs {
|
||||
}
|
||||
*/
|
||||
|
||||
/// Get all logs that exist for a given profile
|
||||
/// Get all logs that exist for a given instance
|
||||
/// This is returned as an array of Log objects, sorted by filename (the folder name, when the log was created)
|
||||
export async function get_logs(profilePath, clearContents) {
|
||||
return await invoke('plugin:logs|logs_get_logs', { profilePath, clearContents })
|
||||
export async function get_logs(instanceId, clearContents) {
|
||||
return await invoke('plugin:logs|logs_get_logs', { instanceId, clearContents })
|
||||
}
|
||||
|
||||
/// Get a profile's log by filename
|
||||
export async function get_logs_by_filename(profilePath, logType, filename) {
|
||||
return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, logType, filename })
|
||||
/// Get an instance's log by filename
|
||||
export async function get_logs_by_filename(instanceId, logType, filename) {
|
||||
return await invoke('plugin:logs|logs_get_logs_by_filename', { instanceId, logType, filename })
|
||||
}
|
||||
|
||||
/// Get a profile's log text only by filename
|
||||
export async function get_output_by_filename(profilePath, logType, filename) {
|
||||
/// Get an instance's log text only by filename
|
||||
export async function get_output_by_filename(instanceId, logType, filename) {
|
||||
return await invoke('plugin:logs|logs_get_output_by_filename', {
|
||||
profilePath,
|
||||
instanceId,
|
||||
logType,
|
||||
filename,
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a profile's log by filename
|
||||
export async function delete_logs_by_filename(profilePath, logType, filename) {
|
||||
/// Delete an instance's log by filename
|
||||
export async function delete_logs_by_filename(instanceId, logType, filename) {
|
||||
return await invoke('plugin:logs|logs_delete_logs_by_filename', {
|
||||
profilePath,
|
||||
instanceId,
|
||||
logType,
|
||||
filename,
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete all logs for a given profile
|
||||
export async function delete_logs(profilePath) {
|
||||
return await invoke('plugin:logs|logs_delete_logs', { profilePath })
|
||||
/// Delete all logs for a given instance
|
||||
export async function delete_logs(instanceId) {
|
||||
return await invoke('plugin:logs|logs_delete_logs', { instanceId })
|
||||
}
|
||||
|
||||
/// Get the latest log for a given profile and cursor (startpoint to read withi nthe file)
|
||||
/// Get the latest log for a given instance and cursor (startpoint to read within the file)
|
||||
/// Returns:
|
||||
/*
|
||||
{
|
||||
@@ -59,16 +59,16 @@ export async function delete_logs(profilePath) {
|
||||
*/
|
||||
|
||||
// From latest.log directly
|
||||
export async function get_latest_log_cursor(profilePath, cursor) {
|
||||
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })
|
||||
export async function get_latest_log_cursor(instanceId, cursor) {
|
||||
return await invoke('plugin:logs|logs_get_latest_log_cursor', { instanceId, cursor })
|
||||
}
|
||||
|
||||
/// Get all buffered live log lines for a profile from the Rust ring buffer
|
||||
export async function get_live_log_buffer(profilePath) {
|
||||
return await invoke('plugin:logs|logs_get_live_log_buffer', { profilePath })
|
||||
/// Get all buffered live log lines for an instance from the Rust ring buffer
|
||||
export async function get_live_log_buffer(instanceId) {
|
||||
return await invoke('plugin:logs|logs_get_live_log_buffer', { instanceId })
|
||||
}
|
||||
|
||||
/// Clear the live log buffer for a profile on the Rust side
|
||||
export async function clear_log_buffer(profilePath) {
|
||||
return await invoke('plugin:logs|logs_clear_live_log_buffer', { profilePath })
|
||||
/// Clear the live log buffer for an instance on the Rust side
|
||||
export async function clear_log_buffer(instanceId) {
|
||||
return await invoke('plugin:logs|logs_clear_live_log_buffer', { instanceId })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
@@ -12,8 +12,10 @@ export type ModrinthCredentials = {
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export async function login(): Promise<ModrinthCredentials> {
|
||||
return await invoke('plugin:mr-auth|modrinth_login')
|
||||
export type ModrinthAuthFlow = 'sign-in' | 'sign-up'
|
||||
|
||||
export async function login(flow: ModrinthAuthFlow = 'sign-in'): Promise<ModrinthCredentials> {
|
||||
return await invoke('plugin:mr-auth|modrinth_login', { flow })
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import { create } from './profile'
|
||||
import type { InstanceLoader } from './types'
|
||||
|
||||
interface PackProfileCreator {
|
||||
name: string
|
||||
gameVersion: string
|
||||
modloader: InstanceLoader
|
||||
loaderVersion: string | null
|
||||
unknownFile: boolean
|
||||
}
|
||||
|
||||
interface PackLocationVersionId {
|
||||
type: 'fromVersionId'
|
||||
project_id: string
|
||||
version_id: string
|
||||
title: string
|
||||
icon_url?: string
|
||||
}
|
||||
|
||||
interface PackLocationFile {
|
||||
type: 'fromFile'
|
||||
path: string
|
||||
}
|
||||
|
||||
export async function create_profile_and_install(
|
||||
projectId: string,
|
||||
versionId: string,
|
||||
packTitle: string,
|
||||
iconUrl?: string,
|
||||
createInstanceCallback: (profile: string) => void = () => {},
|
||||
): Promise<void> {
|
||||
const location: PackLocationVersionId = {
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
version_id: versionId,
|
||||
title: packTitle,
|
||||
icon_url: iconUrl,
|
||||
}
|
||||
const profile_creator = await invoke<PackProfileCreator>(
|
||||
'plugin:pack|pack_get_profile_from_pack',
|
||||
{ location },
|
||||
)
|
||||
const profile = await create(
|
||||
profile_creator.name,
|
||||
profile_creator.gameVersion,
|
||||
profile_creator.modloader,
|
||||
profile_creator.loaderVersion,
|
||||
null,
|
||||
true,
|
||||
)
|
||||
createInstanceCallback(profile)
|
||||
|
||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||
}
|
||||
|
||||
export async function install_to_existing_profile(
|
||||
projectId: string,
|
||||
versionId: string,
|
||||
title: string,
|
||||
profilePath: string,
|
||||
): Promise<void> {
|
||||
const location: PackLocationVersionId = {
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
version_id: versionId,
|
||||
title,
|
||||
}
|
||||
return await invoke('plugin:pack|pack_install', { location, profile: profilePath })
|
||||
}
|
||||
|
||||
export async function create_profile_and_install_from_file(
|
||||
path: string,
|
||||
showUnknownPackWarningModal?: (createProfile: () => Promise<void>, fileName: string) => void,
|
||||
): Promise<void> {
|
||||
const location: PackLocationFile = {
|
||||
type: 'fromFile',
|
||||
path,
|
||||
}
|
||||
const profile_creator = await invoke<PackProfileCreator>(
|
||||
'plugin:pack|pack_get_profile_from_pack',
|
||||
{ location },
|
||||
)
|
||||
|
||||
const createProfile = async () => {
|
||||
const profile = await create(
|
||||
profile_creator.name,
|
||||
profile_creator.gameVersion,
|
||||
profile_creator.modloader,
|
||||
profile_creator.loaderVersion,
|
||||
null,
|
||||
true,
|
||||
)
|
||||
await invoke('plugin:pack|pack_install', { location, profile })
|
||||
}
|
||||
|
||||
if (profile_creator.unknownFile && showUnknownPackWarningModal) {
|
||||
const splitPath = path.split(/[\\/]/)
|
||||
const fileName = splitPath ? splitPath[splitPath.length - 1] : path
|
||||
showUnknownPackWarningModal(createProfile, fileName)
|
||||
} else {
|
||||
await createProfile()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
/// Gets all running process IDs with a given profile path
|
||||
/// Gets all running process IDs with a given instance ID
|
||||
/// Returns [u32]
|
||||
export async function get_by_profile_path(path) {
|
||||
return await invoke('plugin:process|process_get_by_profile_path', { path })
|
||||
export async function get_by_instance_id(instanceId) {
|
||||
return await invoke('plugin:process|process_get_by_instance_id', { instanceId })
|
||||
}
|
||||
|
||||
/// Gets all running process IDs with a given profile path
|
||||
/// Gets all running process IDs
|
||||
/// Returns [u32]
|
||||
export async function get_all() {
|
||||
return await invoke('plugin:process|process_get_all')
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { ContentItem, ContentOwner } from '@modrinth/ui'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import { install_to_existing_profile } from '@/helpers/pack'
|
||||
|
||||
import type {
|
||||
CacheBehaviour,
|
||||
ContentFile,
|
||||
ContentFileProjectType,
|
||||
GameInstance,
|
||||
InstanceLoader,
|
||||
} from './types'
|
||||
|
||||
// Add instance
|
||||
/*
|
||||
name: String, // the name of the profile, and relative path to create
|
||||
game_version: String, // the game version of the profile
|
||||
modloader: ModLoader, // the modloader to use
|
||||
- ModLoader is an enum, with the following variants: Vanilla, Forge, Fabric, Quilt
|
||||
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||
icon: Path, // the icon for the profile
|
||||
- icon is a path to an image file, which will be copied into the profile directory
|
||||
*/
|
||||
|
||||
export async function create(
|
||||
name: string,
|
||||
gameVersion: string,
|
||||
modloader: InstanceLoader,
|
||||
loaderVersion: string | null,
|
||||
icon: string | null,
|
||||
skipInstall: boolean,
|
||||
linkedData?: { project_id: string; version_id: string; locked: boolean } | null,
|
||||
): Promise<string> {
|
||||
// Trim string name to avoid "Unable to find directory"
|
||||
name = name.trim()
|
||||
return await invoke('plugin:profile-create|profile_create', {
|
||||
name,
|
||||
gameVersion,
|
||||
modloader,
|
||||
loaderVersion,
|
||||
icon,
|
||||
skipInstall,
|
||||
linkedData,
|
||||
})
|
||||
}
|
||||
|
||||
// duplicate a profile
|
||||
export async function duplicate(path: string): Promise<string> {
|
||||
return await invoke('plugin:profile-create|profile_duplicate', { path })
|
||||
}
|
||||
|
||||
// Remove a profile
|
||||
export async function remove(path: string): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_remove', { path })
|
||||
}
|
||||
|
||||
// Get a profile by path
|
||||
// Returns a Profile
|
||||
export async function get(path: string): Promise<GameInstance | null> {
|
||||
return await invoke('plugin:profile|profile_get', { path })
|
||||
}
|
||||
|
||||
export async function get_many(paths: string[]): Promise<GameInstance[]> {
|
||||
return await invoke('plugin:profile|profile_get_many', { paths })
|
||||
}
|
||||
|
||||
// Get a profile's projects
|
||||
// Returns a map of a path to profile file
|
||||
export async function get_projects(
|
||||
path: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<Record<string, ContentFile>> {
|
||||
return await invoke('plugin:profile|profile_get_projects', { path, cacheBehaviour })
|
||||
}
|
||||
|
||||
// Get just the installed project IDs for a profile (lightweight, skips update checks)
|
||||
export async function get_installed_project_ids(path: string): Promise<string[]> {
|
||||
return await invoke('plugin:profile|profile_get_installed_project_ids', { path })
|
||||
}
|
||||
|
||||
// Get content items with rich metadata for a profile
|
||||
// Returns content items filtered to exclude modpack files (if linked),
|
||||
// sorted alphabetically by project name
|
||||
export async function get_content_items(
|
||||
path: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<ContentItem[]> {
|
||||
return await invoke('plugin:profile|profile_get_content_items', { path, cacheBehaviour })
|
||||
}
|
||||
|
||||
// Linked modpack info returned from backend
|
||||
export interface LinkedModpackInfo {
|
||||
project: Labrinth.Projects.v2.Project
|
||||
version: Labrinth.Versions.v2.Version
|
||||
owner: ContentOwner | null
|
||||
has_update: boolean
|
||||
update_version_id: string | null
|
||||
update_version: Labrinth.Versions.v2.Version | null
|
||||
}
|
||||
|
||||
// Get linked modpack info for a profile
|
||||
// Returns project, version, and owner information for the linked modpack,
|
||||
// or null if the profile is not linked to a modpack
|
||||
export async function get_linked_modpack_info(
|
||||
path: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<LinkedModpackInfo | null> {
|
||||
return await invoke('plugin:profile|profile_get_linked_modpack_info', { path, cacheBehaviour })
|
||||
}
|
||||
|
||||
// Get content items that are part of the linked modpack
|
||||
// Returns the modpack's dependencies as ContentItem list
|
||||
// Returns empty array if the profile is not linked to a modpack
|
||||
export async function get_linked_modpack_content(
|
||||
path: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<ContentItem[]> {
|
||||
return await invoke('plugin:profile|profile_get_linked_modpack_content', { path, cacheBehaviour })
|
||||
}
|
||||
|
||||
// Convert a list of dependencies into ContentItems with rich metadata
|
||||
export async function get_dependencies_as_content_items(
|
||||
dependencies: Labrinth.Versions.v3.Dependency[],
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
): Promise<ContentItem[]> {
|
||||
return await invoke('plugin:profile|profile_get_dependencies_as_content_items', {
|
||||
dependencies,
|
||||
cacheBehaviour,
|
||||
})
|
||||
}
|
||||
|
||||
// Get a profile's full fs path
|
||||
// Returns a path
|
||||
export async function get_full_path(path: string): Promise<string> {
|
||||
return await invoke('plugin:profile|profile_get_full_path', { path })
|
||||
}
|
||||
|
||||
// Get's a mod's full fs path
|
||||
// Returns a path
|
||||
export async function get_mod_full_path(path: string, projectPath: string): Promise<string> {
|
||||
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<JavaVersion | null> {
|
||||
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
|
||||
}
|
||||
|
||||
// Get a copy of the profile set
|
||||
// Returns hashmap of path -> Profile
|
||||
export async function list(): Promise<GameInstance[]> {
|
||||
return await invoke('plugin:profile|profile_list')
|
||||
}
|
||||
|
||||
export async function check_installed(path: string, projectId: string): Promise<boolean> {
|
||||
return await invoke('plugin:profile|profile_check_installed', { path, projectId })
|
||||
}
|
||||
|
||||
export async function check_installed_batch(projectId: string): Promise<Record<string, boolean>> {
|
||||
return await invoke('plugin:profile|profile_check_installed_batch', { projectId })
|
||||
}
|
||||
|
||||
// Installs/Repairs a profile
|
||||
export async function install(path: string, force: boolean): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_install', { path, force })
|
||||
}
|
||||
|
||||
// Updates all of a profile's projects
|
||||
export async function update_all(path: string): Promise<Record<string, string>> {
|
||||
return await invoke('plugin:profile|profile_update_all', { path })
|
||||
}
|
||||
|
||||
// Updates a specified project
|
||||
export async function update_project(path: string, projectPath: string): Promise<string> {
|
||||
return await invoke('plugin:profile|profile_update_project', { path, projectPath })
|
||||
}
|
||||
|
||||
// Add a project to a profile from a version
|
||||
// Returns a path to the new project file
|
||||
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
|
||||
|
||||
export async function add_project_from_version(
|
||||
path: string,
|
||||
versionId: string,
|
||||
reason: DownloadReason,
|
||||
dependentOnVersionId?: string,
|
||||
): Promise<string> {
|
||||
return await invoke('plugin:profile|profile_add_project_from_version', {
|
||||
path,
|
||||
versionId,
|
||||
reason,
|
||||
dependentOnVersionId,
|
||||
})
|
||||
}
|
||||
|
||||
// Add a project to a profile from a path + project_type
|
||||
// Returns a path to the new project file
|
||||
export async function add_project_from_path(
|
||||
path: string,
|
||||
projectPath: string,
|
||||
projectType?: ContentFileProjectType,
|
||||
): Promise<string> {
|
||||
return await invoke('plugin:profile|profile_add_project_from_path', {
|
||||
path,
|
||||
projectPath,
|
||||
projectType,
|
||||
})
|
||||
}
|
||||
|
||||
// Toggle disabling a project
|
||||
export async function toggle_disable_project(path: string, projectPath: string): Promise<string> {
|
||||
return await invoke('plugin:profile|profile_toggle_disable_project', { path, projectPath })
|
||||
}
|
||||
|
||||
// Remove a project
|
||||
export async function remove_project(path: string, projectPath: string): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_remove_project', { path, projectPath })
|
||||
}
|
||||
|
||||
// Update a managed Modrinth profile to a specific version
|
||||
export async function update_managed_modrinth_version(
|
||||
path: string,
|
||||
versionId: string,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_update_managed_modrinth_version', {
|
||||
path,
|
||||
versionId,
|
||||
})
|
||||
}
|
||||
|
||||
// Repair a managed Modrinth profile
|
||||
export async function update_repair_modrinth(path: string): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
|
||||
}
|
||||
|
||||
// Export a profile to .mrpack
|
||||
// included_overrides is an array of paths to override folders to include (ie: 'mods', 'resource_packs')
|
||||
// Version id is optional (ie: 1.1.5)
|
||||
export async function export_profile_mrpack(
|
||||
path: string,
|
||||
exportLocation: string,
|
||||
includedOverrides: string[],
|
||||
versionId?: string,
|
||||
description?: string,
|
||||
name?: string,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_export_mrpack', {
|
||||
path,
|
||||
exportLocation,
|
||||
includedOverrides,
|
||||
versionId,
|
||||
description,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
// Given a folder path, populate an array of all the subfolders
|
||||
// Intended to be used for finding potential override folders
|
||||
// profile
|
||||
// -- mods
|
||||
// -- resourcepacks
|
||||
// -- file1
|
||||
// => [mods, resourcepacks]
|
||||
// allows selection for 'included_overrides' in export_profile_mrpack
|
||||
export async function get_pack_export_candidates(profilePath: string): Promise<string[]> {
|
||||
return await invoke('plugin:profile|profile_get_pack_export_candidates', { profilePath })
|
||||
}
|
||||
|
||||
// Run Minecraft using a pathed profile
|
||||
// Returns PID of child
|
||||
export async function run(path: string, serverAddress: string | null = null): Promise<unknown> {
|
||||
return await invoke('plugin:profile|profile_run', { path, serverAddress })
|
||||
}
|
||||
|
||||
export async function kill(path: string): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_kill', { path })
|
||||
}
|
||||
|
||||
// Edits a profile
|
||||
export async function edit(path: string, editProfile: Partial<GameInstance>): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_edit', { path, editProfile })
|
||||
}
|
||||
|
||||
// Edits a profile's icon
|
||||
export async function edit_icon(path: string, iconPath: string | null): Promise<void> {
|
||||
return await invoke('plugin:profile|profile_edit_icon', { path, iconPath })
|
||||
}
|
||||
|
||||
export async function finish_install(instance: GameInstance): Promise<void> {
|
||||
if (instance.install_stage !== 'pack_installed') {
|
||||
const linkedData = instance.linked_data
|
||||
if (linkedData) {
|
||||
await install_to_existing_profile(
|
||||
linkedData.project_id,
|
||||
linkedData.version_id,
|
||||
instance.name,
|
||||
instance.path,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
await install(instance.path, false)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
|
||||
import { getErrorMessage, type SharedInstanceUnavailableReason } from '@/helpers/install'
|
||||
|
||||
export const sharedInstanceErrorMessages = defineMessages({
|
||||
unavailableTitle: {
|
||||
id: 'instance.shared-instance.unavailable.title',
|
||||
defaultMessage: 'Shared instance no longer available',
|
||||
},
|
||||
unavailableText: {
|
||||
id: 'instance.shared-instance.unavailable.text-v2',
|
||||
defaultMessage:
|
||||
"Your local instance is still available, but it is no longer linked and won't receive updates.",
|
||||
},
|
||||
deletedText: {
|
||||
id: 'instance.shared-instance.unavailable.deleted-text-v2',
|
||||
defaultMessage:
|
||||
"The shared instance was deleted. Your local instance is still available, but it is no longer linked and won't receive updates.",
|
||||
},
|
||||
accessRevokedText: {
|
||||
id: 'instance.shared-instance.unavailable.access-revoked-text-v2',
|
||||
defaultMessage:
|
||||
"Your access to the shared instance was revoked. Your local instance is still available, but it is no longer linked and won't receive updates.",
|
||||
},
|
||||
unavailableFallbackManager: {
|
||||
id: 'instance.shared-instance.unavailable.manager-fallback',
|
||||
defaultMessage: 'the instance manager',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'instance.shared-instance.error.title',
|
||||
defaultMessage: 'Something has gone wrong',
|
||||
},
|
||||
})
|
||||
|
||||
export function sharedInstanceUnavailableTextMessage(
|
||||
reason: SharedInstanceUnavailableReason | null,
|
||||
) {
|
||||
if (reason === 'deleted') return sharedInstanceErrorMessages.deletedText
|
||||
if (reason === 'access_revoked') return sharedInstanceErrorMessages.accessRevokedText
|
||||
return sharedInstanceErrorMessages.unavailableText
|
||||
}
|
||||
|
||||
export function useSharedInstanceErrors() {
|
||||
const { formatMessage } = useVIntl()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
function formatSharedInstanceUnavailable(
|
||||
reason: SharedInstanceUnavailableReason | null = null,
|
||||
manager?: string | null,
|
||||
) {
|
||||
return formatMessage(sharedInstanceUnavailableTextMessage(reason), {
|
||||
manager: manager || formatMessage(sharedInstanceErrorMessages.unavailableFallbackManager),
|
||||
})
|
||||
}
|
||||
|
||||
function notifySharedInstanceUnavailable(
|
||||
reason: SharedInstanceUnavailableReason | null = null,
|
||||
manager?: string | null,
|
||||
) {
|
||||
addNotification({
|
||||
type: 'warning',
|
||||
title: formatMessage(sharedInstanceErrorMessages.unavailableTitle),
|
||||
text: formatSharedInstanceUnavailable(reason, manager),
|
||||
})
|
||||
}
|
||||
|
||||
function notifySharedInstanceError(error: unknown) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(sharedInstanceErrorMessages.errorTitle),
|
||||
text: getErrorMessage(error),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
formatSharedInstanceUnavailable,
|
||||
notifySharedInstanceError,
|
||||
notifySharedInstanceUnavailable,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
@@ -8,7 +8,8 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
export interface LoadingBarType {
|
||||
type?: string
|
||||
version?: string
|
||||
profile_path?: string
|
||||
instance_id?: string
|
||||
instance_name?: string
|
||||
pack_name?: string
|
||||
icon?: string | null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
+90
-7
@@ -1,20 +1,24 @@
|
||||
import type { ModrinthId } from '@modrinth/utils'
|
||||
|
||||
export type GameInstance = {
|
||||
id: string
|
||||
path: string
|
||||
install_stage: InstallStage
|
||||
launcher_feature_version: string
|
||||
|
||||
name: string
|
||||
icon_path?: string
|
||||
|
||||
game_version: string
|
||||
protocol_version?: number
|
||||
loader: InstanceLoader
|
||||
loader_version?: string
|
||||
|
||||
groups: string[]
|
||||
|
||||
linked_data?: LinkedData
|
||||
preferred_update_channel: ReleaseChannel
|
||||
link?: InstanceLink | null
|
||||
shared_instance?: SharedInstanceAttachment | null
|
||||
update_channel: ReleaseChannel
|
||||
|
||||
created: Date
|
||||
modified: Date
|
||||
@@ -40,18 +44,90 @@ type InstallStage =
|
||||
| 'pack_installing'
|
||||
| 'not_installed'
|
||||
|
||||
type LinkedData = {
|
||||
project_id: ModrinthId
|
||||
version_id: ModrinthId
|
||||
|
||||
locked: boolean
|
||||
type InstanceLinkIdentity = {
|
||||
project_id?: ModrinthId | null
|
||||
version_id?: ModrinthId | null
|
||||
server_project_id?: ModrinthId | null
|
||||
content_project_id?: ModrinthId | null
|
||||
content_version_id?: ModrinthId | null
|
||||
}
|
||||
|
||||
export type InstanceLink = InstanceLinkIdentity &
|
||||
(
|
||||
| {
|
||||
type: 'modrinth_modpack'
|
||||
project_id: ModrinthId
|
||||
version_id: ModrinthId
|
||||
}
|
||||
| {
|
||||
type: 'server_project'
|
||||
project_id: ModrinthId
|
||||
}
|
||||
| {
|
||||
type: 'server_project_modpack'
|
||||
server_project_id: ModrinthId
|
||||
content_project_id?: ModrinthId | null
|
||||
content_version_id: ModrinthId
|
||||
project_id?: ModrinthId
|
||||
version_id?: ModrinthId
|
||||
}
|
||||
| {
|
||||
type: 'imported_modpack'
|
||||
project_id?: ModrinthId | null
|
||||
version_id?: ModrinthId | null
|
||||
name?: string | null
|
||||
version_number?: string | null
|
||||
filename?: string | null
|
||||
}
|
||||
| {
|
||||
type: 'modrinth_hosting'
|
||||
server_id: string
|
||||
instance_ids: string[]
|
||||
active_instance_id?: string | null
|
||||
}
|
||||
| {
|
||||
type: 'shared_instance'
|
||||
modpack_project_id?: ModrinthId | null
|
||||
modpack_version_id?: ModrinthId | null
|
||||
}
|
||||
)
|
||||
|
||||
export type SharedInstanceAttachment = {
|
||||
id: string
|
||||
role: 'owner' | 'member'
|
||||
manager_id?: string | null
|
||||
server_manager_name?: string | null
|
||||
server_manager_icon_url?: string | null
|
||||
linked_user_id?: string | null
|
||||
status:
|
||||
| 'unknown'
|
||||
| 'up_to_date'
|
||||
| 'update_available'
|
||||
| 'applying'
|
||||
| 'stale'
|
||||
| 'not_ready'
|
||||
| 'error'
|
||||
applied_version?: number | null
|
||||
latest_version?: number | null
|
||||
}
|
||||
|
||||
export type Instance = GameInstance
|
||||
|
||||
type ReleaseChannel = 'release' | 'beta' | 'alpha'
|
||||
|
||||
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||
|
||||
export type ContentSourceKind =
|
||||
| 'local'
|
||||
| 'modrinth_modpack'
|
||||
| 'server_project'
|
||||
| 'modrinth_hosting'
|
||||
| 'imported_modpack'
|
||||
| 'shared_instance'
|
||||
|
||||
type ContentFile = {
|
||||
enabled: boolean
|
||||
source_kind?: ContentSourceKind | null
|
||||
metadata?: {
|
||||
project_id: string
|
||||
version_id: string
|
||||
@@ -87,11 +163,18 @@ type Hooks = {
|
||||
|
||||
type Manifest = {
|
||||
gameVersions: ManifestGameVersion[]
|
||||
versionGroups?: ManifestVersionGroup[]
|
||||
}
|
||||
|
||||
type ManifestGameVersion = {
|
||||
id: string
|
||||
stable: boolean
|
||||
versionGroup?: string
|
||||
loaders: ManifestLoaderVersion[]
|
||||
}
|
||||
|
||||
type ManifestVersionGroup = {
|
||||
id: string
|
||||
loaders: ManifestLoaderVersion[]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export type SearchUser = {
|
||||
id: string
|
||||
username: string
|
||||
avatar_url: string | null
|
||||
}
|
||||
|
||||
export async function search_user(query: string): Promise<SearchUser[]> {
|
||||
return await invoke<SearchUser[]>('plugin:users|search_user', { query })
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
|
||||
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
|
||||
import { get_full_path, get_mod_full_path } from '@/helpers/instance'
|
||||
|
||||
export async function isDev() {
|
||||
return await invoke('is_dev')
|
||||
@@ -47,14 +48,34 @@ export async function showLauncherLogsFolder() {
|
||||
return await invoke('plugin:utils|show_launcher_logs_folder', {})
|
||||
}
|
||||
|
||||
// Opens a profile's folder in the OS file explorer
|
||||
export async function showProfileInFolder(path) {
|
||||
const fullPath = await get_full_path(path)
|
||||
export async function createInstanceShortcut(instanceName, instanceId, options = {}) {
|
||||
const outputPath = await save({
|
||||
defaultPath: `Modrinth - ${instanceName}`,
|
||||
})
|
||||
|
||||
if (!outputPath) return null
|
||||
|
||||
return await invoke('plugin:shortcuts|create_instance_shortcut', {
|
||||
instanceName,
|
||||
instanceId,
|
||||
outputPath,
|
||||
server: options.server,
|
||||
singleplayerWorld: options.singleplayerWorld,
|
||||
})
|
||||
}
|
||||
|
||||
export async function showAppDbBackupsFolder() {
|
||||
return await invoke('plugin:utils|show_app_db_backups_folder', {})
|
||||
}
|
||||
|
||||
// Opens an instance's folder in the OS file explorer
|
||||
export async function showInstanceInFolder(instanceId) {
|
||||
const fullPath = await get_full_path(instanceId)
|
||||
return await openPath(fullPath)
|
||||
}
|
||||
|
||||
export async function highlightModInProfile(profilePath, projectPath) {
|
||||
const fullPath = await get_mod_full_path(profilePath, projectPath)
|
||||
export async function highlightModInInstance(instanceId, projectPath) {
|
||||
const fullPath = await get_mod_full_path(instanceId, projectPath)
|
||||
return await highlightInFolder(fullPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { autoToHTML } from '@sfirew/minecraft-motd-parser'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { get_full_path } from '@/helpers/profile'
|
||||
import { get_full_path } from '@/helpers/instance'
|
||||
import { openPath } from '@/helpers/utils'
|
||||
|
||||
type BaseWorld = {
|
||||
@@ -36,8 +36,8 @@ export type ServerWorld = BaseWorld & {
|
||||
|
||||
export type World = SingleplayerWorld | ServerWorld
|
||||
|
||||
export type WorldWithProfile = {
|
||||
profile: string
|
||||
export type WorldWithInstance = {
|
||||
instance_id: string
|
||||
} & World
|
||||
|
||||
export type SingleplayerGameMode = 'survival' | 'creative' | 'adventure' | 'spectator'
|
||||
@@ -88,12 +88,12 @@ export type ProtocolVersion = {
|
||||
export async function get_recent_worlds(
|
||||
limit: number,
|
||||
displayStatuses?: DisplayStatus[],
|
||||
): Promise<WorldWithProfile[]> {
|
||||
): Promise<WorldWithInstance[]> {
|
||||
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
|
||||
}
|
||||
|
||||
export async function get_profile_worlds(path: string): Promise<World[]> {
|
||||
return await invoke('plugin:worlds|get_profile_worlds', { path })
|
||||
export async function get_instance_worlds(instanceId: string): Promise<World[]> {
|
||||
return await invoke('plugin:worlds|get_instance_worlds', { instanceId })
|
||||
}
|
||||
|
||||
export async function get_singleplayer_world(
|
||||
@@ -137,16 +137,16 @@ export async function delete_world(instance: string, world: string): Promise<voi
|
||||
return await invoke('plugin:worlds|delete_world', { instance, world })
|
||||
}
|
||||
|
||||
export async function add_server_to_profile(
|
||||
path: string,
|
||||
export async function add_server_to_instance(
|
||||
instanceId: string,
|
||||
name: string,
|
||||
address: string,
|
||||
packStatus: ServerPackStatus,
|
||||
projectId?: string,
|
||||
contentKind?: string,
|
||||
): Promise<number> {
|
||||
return await invoke('plugin:worlds|add_server_to_profile', {
|
||||
path,
|
||||
return await invoke('plugin:worlds|add_server_to_instance', {
|
||||
instanceId,
|
||||
name,
|
||||
address,
|
||||
packStatus,
|
||||
@@ -155,15 +155,15 @@ export async function add_server_to_profile(
|
||||
})
|
||||
}
|
||||
|
||||
export async function edit_server_in_profile(
|
||||
path: string,
|
||||
export async function edit_server_in_instance(
|
||||
instanceId: string,
|
||||
index: number,
|
||||
name: string,
|
||||
address: string,
|
||||
packStatus: ServerPackStatus,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:worlds|edit_server_in_profile', {
|
||||
path,
|
||||
return await invoke('plugin:worlds|edit_server_in_instance', {
|
||||
instanceId,
|
||||
index,
|
||||
name,
|
||||
address,
|
||||
@@ -171,12 +171,17 @@ export async function edit_server_in_profile(
|
||||
})
|
||||
}
|
||||
|
||||
export async function remove_server_from_profile(path: string, index: number): Promise<void> {
|
||||
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
||||
export async function remove_server_from_instance(
|
||||
instanceId: string,
|
||||
index: number,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:worlds|remove_server_from_instance', { instanceId, index })
|
||||
}
|
||||
|
||||
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
||||
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
||||
export async function get_instance_protocol_version(
|
||||
instanceId: string,
|
||||
): Promise<ProtocolVersion | null> {
|
||||
return await invoke('plugin:worlds|get_instance_protocol_version', { instanceId })
|
||||
}
|
||||
|
||||
export async function get_server_status(
|
||||
@@ -186,16 +191,19 @@ export async function get_server_status(
|
||||
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
||||
}
|
||||
|
||||
export async function start_join_singleplayer_world(path: string, world: string): Promise<unknown> {
|
||||
return await invoke('plugin:worlds|start_join_singleplayer_world', { path, world })
|
||||
export async function start_join_singleplayer_world(
|
||||
instanceId: string,
|
||||
world: string,
|
||||
): Promise<unknown> {
|
||||
return await invoke('plugin:worlds|start_join_singleplayer_world', { instanceId, world })
|
||||
}
|
||||
|
||||
export async function start_join_server(path: string, address: string): Promise<unknown> {
|
||||
return await invoke('plugin:worlds|start_join_server', { path, address })
|
||||
export async function start_join_server(instanceId: string, address: string): Promise<unknown> {
|
||||
return await invoke('plugin:worlds|start_join_server', { instanceId, address })
|
||||
}
|
||||
|
||||
export async function showWorldInFolder(instancePath: string, worldPath: string) {
|
||||
const fullPath = await get_full_path(instancePath)
|
||||
export async function showWorldInFolder(instanceId: string, worldPath: string) {
|
||||
const fullPath = await get_full_path(instanceId)
|
||||
return await openPath(fullPath + '/saves/' + worldPath)
|
||||
}
|
||||
|
||||
@@ -348,6 +356,28 @@ export function resolveManagedServerWorld(
|
||||
)
|
||||
}
|
||||
|
||||
export function getServerAddress(javaServer?: { address?: string | null } | null) {
|
||||
if (!javaServer) return null
|
||||
return javaServer.address ?? null
|
||||
}
|
||||
|
||||
export async function ensureManagedServerWorldExists(
|
||||
instanceId: string,
|
||||
serverName: string,
|
||||
serverAddress: string | null,
|
||||
) {
|
||||
if (!instanceId || !serverAddress) return
|
||||
try {
|
||||
const worlds = await get_instance_worlds(instanceId)
|
||||
const managedWorld = resolveManagedServerWorld(worlds, serverName, serverAddress)
|
||||
if (!managedWorld) {
|
||||
await add_server_to_instance(instanceId, serverName, serverAddress, 'prompt')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to ensure managed server world exists:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getServerLatency(
|
||||
address: string,
|
||||
protocolVersion: ProtocolVersion | null = null,
|
||||
@@ -374,28 +404,35 @@ export async function refreshServerData(
|
||||
): Promise<void> {
|
||||
const refreshTime = Date.now()
|
||||
serverData.refreshing = true
|
||||
await get_server_status(address, protocolVersion)
|
||||
.then((status) => {
|
||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||
// Don't update if there was a more recent successful refresh
|
||||
return
|
||||
}
|
||||
serverData.lastSuccessfulRefresh = Date.now()
|
||||
serverData.status = status
|
||||
if (status.description) {
|
||||
serverData.rawMotd = status.description
|
||||
serverData.renderedMotd = autoToHTML(status.description)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
serverData.refreshing = false
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||
if (!protocolVersion?.legacy) {
|
||||
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||
}
|
||||
})
|
||||
try {
|
||||
const status = await get_server_status(address, protocolVersion)
|
||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||
// Don't update if there was a more recent successful refresh
|
||||
return
|
||||
}
|
||||
serverData.lastSuccessfulRefresh = Date.now()
|
||||
serverData.status = status
|
||||
if (status.description) {
|
||||
serverData.rawMotd = status.description
|
||||
serverData.renderedMotd = autoToHTML(status.description)
|
||||
} else {
|
||||
delete serverData.rawMotd
|
||||
delete serverData.renderedMotd
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||
if (!protocolVersion?.legacy) {
|
||||
await refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||
return
|
||||
}
|
||||
if (!serverData.lastSuccessfulRefresh || serverData.lastSuccessfulRefresh <= refreshTime) {
|
||||
delete serverData.status
|
||||
delete serverData.rawMotd
|
||||
delete serverData.renderedMotd
|
||||
}
|
||||
} finally {
|
||||
serverData.refreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshServers(
|
||||
@@ -414,15 +451,15 @@ export function refreshServers(
|
||||
}
|
||||
})
|
||||
|
||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||
// noinspection ES6MissingAwait - handled by refreshServerData
|
||||
Object.keys(serverData).forEach((address) =>
|
||||
refreshServerData(serverData[address], protocolVersion, address),
|
||||
)
|
||||
}
|
||||
|
||||
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
||||
export async function refreshWorld(worlds: World[], instanceId: string, worldPath: string) {
|
||||
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
||||
const newWorld = await get_singleplayer_world(instancePath, worldPath)
|
||||
const newWorld = await get_singleplayer_world(instanceId, worldPath)
|
||||
if (index !== -1) {
|
||||
worlds[index] = newWorld
|
||||
} else {
|
||||
@@ -432,13 +469,13 @@ export async function refreshWorld(worlds: World[], instancePath: string, worldP
|
||||
sortWorlds(worlds)
|
||||
}
|
||||
|
||||
export async function handleDefaultProfileUpdateEvent(
|
||||
export async function handleDefaultInstanceUpdateEvent(
|
||||
worlds: World[],
|
||||
instancePath: string,
|
||||
e: ProfileEvent,
|
||||
instanceId: string,
|
||||
e: InstanceEvent,
|
||||
) {
|
||||
if (e.event === 'world_updated') {
|
||||
await refreshWorld(worlds, instancePath, e.world)
|
||||
await refreshWorld(worlds, instanceId, e.world)
|
||||
}
|
||||
|
||||
if (e.event === 'server_joined') {
|
||||
@@ -456,9 +493,9 @@ export async function handleDefaultProfileUpdateEvent(
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshWorlds(instancePath: string): Promise<World[]> {
|
||||
const worlds = await get_profile_worlds(instancePath).catch((err) => {
|
||||
console.error(`Error refreshing worlds for instance: ${instancePath}`, err)
|
||||
export async function refreshWorlds(instanceId: string): Promise<World[]> {
|
||||
const worlds = await get_instance_worlds(instanceId).catch((err) => {
|
||||
console.error(`Error refreshing worlds for instance: ${instanceId}`, err)
|
||||
})
|
||||
if (worlds) {
|
||||
sortWorlds(worlds)
|
||||
@@ -489,7 +526,7 @@ export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVer
|
||||
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
||||
}
|
||||
|
||||
export type ProfileEvent = { profile_path_id: string } & (
|
||||
export type InstanceEvent = { instance_id: string } & (
|
||||
| {
|
||||
event: 'servers_updated'
|
||||
}
|
||||
|
||||
@@ -107,12 +107,6 @@
|
||||
"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": "أدخل وصف التعديل..."
|
||||
},
|
||||
@@ -128,9 +122,6 @@
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "إسم حزمة التعديل"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "أختيار الملفات التي سيتم تصديرها"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "رقم الإصدار"
|
||||
},
|
||||
@@ -185,12 +176,6 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "تصفح الخوادم"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "سيتم حذف '{name}' **نهائيا**, و لن هناك أي طريقة لاسترداده."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "هل أنت متيقِّن من رغبتك بحذف هذا العالم نهائيا؟"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "معدّل"
|
||||
},
|
||||
@@ -209,21 +194,9 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "لم تتم إضافة أي من الخوادم أو العوالم"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "ستتم إزالة '{name}' من قائمتك, إضافة لما داخل اللعبة, و لن يكون هناك أي طريقة لاسترداده."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "ستتم إزالة '{name}' ({address}) من قائمتك, إضافة لما داخل اللعبة, و لن يكون هناك أي طريقة لاسترداده."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "هل أنت متيقِّن من رغبتك في إزالة {name}؟"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "ابحث بين ال{count} عوالم..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "هذا الخادم"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "المحتوى مطلوب"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,21 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Skrýt více spuštěných instancí"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Zavřít"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Otevřít instanci"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Zkusit znovu"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Neznámá instance"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Instalace"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Nastavit jako primární instanci"
|
||||
},
|
||||
@@ -33,7 +48,7 @@
|
||||
"message": "Zastavit instanci"
|
||||
},
|
||||
"app.action-bar.update": {
|
||||
"message": "Aktualizace"
|
||||
"message": "Aktualizovat"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Zobrazit probíhající stahování"
|
||||
@@ -101,8 +116,14 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Zobrazit odehraný čas"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Automaticky přeskočí nízko riziková potrvzení jako duplicitní instalace modpacků, běžné mazání obsahu, hromadné aktualizace, odpojování modpacků a výzvy k opravě. Nebezpečná upozornění se budou nadále zobrazovat."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Přeskočit nedůležitá upozornění"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Umožní přepínat zobrazení postranního panelu."
|
||||
"message": "Umožňí přepínat zobrazení postranního panelu."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Přepnout postranní panel"
|
||||
@@ -141,10 +162,10 @@
|
||||
"message": "Zpět k instanci"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Objevovat obsah"
|
||||
"message": "Objevit obsah"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Objevovat servery"
|
||||
"message": "Objevit servery"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Skrýt již přidané servery"
|
||||
@@ -159,13 +180,7 @@
|
||||
"message": "Instalování"
|
||||
},
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Žádná dostupná verze se neshoduje s verzí {compatibilityLabel}. Vyber verzi, abys ji i přesto nainstaloval/a. Závislosti nebudou automaticky nainstalovány."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Probíhá instalace modpacku..."
|
||||
"message": "Žádné dostupné verze se neshodují s {compatibilityLabel}. Vyberte verzi, kterou chcete přesto nainstalovat. Závislosti nebudou nainstalovány automaticky."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Zadej popis modpacku..."
|
||||
@@ -176,24 +191,72 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportovat modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Zahrnout \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Název modpacku"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Název modpacku"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Číslo verze"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Stahování obsahu"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Stahování Minecraftu"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Stahování pack souboru"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Rozbalování souborů overrides"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Dokončení instalace"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Příprava instance"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Příprava Javy"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Stahování Javy {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Rozbalování Javy {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Získávání Javy {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Připravení Javy {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Validace Javy {version}"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Čtení pack manifestu"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Vyhodnocování loaderu"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Vyhodnocování Minecraftu"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Vyhodnocování obsahu"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Obnovování do předchozího stavu"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Spuštění loader procesů"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Všechna data tvé instance budou trvale smazána, včetně světů, konfigurací a veškerého nainstalovaného obsahu."
|
||||
},
|
||||
@@ -201,7 +264,7 @@
|
||||
"message": "Tato akce nemůže být vrácena"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "Odstranit instanci"
|
||||
"message": "Smazat instanci"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Odstranění instance"
|
||||
@@ -218,6 +281,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instance"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "Stahování {current, number}/{total, number} projektů..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Dokončení aktualizace..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Vyhodnocování verzí..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projekt"
|
||||
},
|
||||
@@ -242,14 +314,20 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Procházet servery"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' bude **navždy smazán** a nebude možné jej obnovit."
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Smazat svět"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Jsi si jistý, že chceš trvale smazat tento svět?"
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Smazat svět"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Tento svět bude trvale smazán z této instance. Tuto akci nelze vrátit zpět."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Mazání {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Módovaný"
|
||||
"message": "Módované"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Offline"
|
||||
@@ -261,26 +339,26 @@
|
||||
"message": "Vanilla"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "Přidej server nebo začni procházet"
|
||||
"message": "Začněte přidáním serveru nebo procházením obsahu"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Žádné servery nebo světy nebyly přidány"
|
||||
"message": "Nejsou přidané žádné servery nebo světy"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' bude odstraněn z tvého seznamu i ze hry a nebude možné jej obnovit."
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Odebrat server"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) bude odstraněn z tvého seznamu i ze hry a nebude možné jej obnovit."
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Odebrat server"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Opravdu chceš odstranit {name}?"
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Tento server bude odebrán z vašeho seznamu serverů i ze seznamu serverů ve hře. Pokud znáte jeho adresu, můžete ho později znovu přidat."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Odebírání {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Hledat v {count} světech..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "tento server"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Požadovaný obsah"
|
||||
},
|
||||
@@ -363,7 +441,7 @@
|
||||
"message": "Smazat skin"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Tahle akce navždy smaže vybraný skin a nejde vrátit zpět."
|
||||
"message": "Tímto dojde k trvalému smazání vybreného skinu. Tuto akci nelze vrátit zpět."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "Opravdu chceš smazat tenhle skin?"
|
||||
@@ -492,19 +570,19 @@
|
||||
"message": "Nadšený Modrinth Bot"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Přihlas se prosím"
|
||||
"message": "Přihlaš se prosím"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Výběr skinu"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Aktualizujte ji znovunačtením nebo se automaticky aktualizuje po zavření."
|
||||
"message": "Stahování Modrinth App v{version} bylo dokončeno. Aktualizujte ji znovunačtením nebo se automaticky aktualizuje po zavření."
|
||||
},
|
||||
"app.update-popup.body.linux": {
|
||||
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte ji pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
|
||||
"message": "Modrinth App v{version} je k dispozici. Aktualizujte ji 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."
|
||||
"message": "Modrinth App 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"
|
||||
@@ -717,13 +795,13 @@
|
||||
"message": "Jméno"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel": {
|
||||
"message": "Aktualizační kanál"
|
||||
"message": "Kanál aktualizací"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.alpha": {
|
||||
"message": "Alfa"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.alpha.description": {
|
||||
"message": "Vydané, beta a alfa verze se zobrazí jako dostupné aktualizace."
|
||||
"message": "Verze typu release, beta a alpha se budou zobrazovat jako dostupné aktualizace."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.beta": {
|
||||
"message": "Beta"
|
||||
@@ -863,6 +941,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Kopírovat adresu"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Vytvořit záložku"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Nezobrazovat na domovské stránce"
|
||||
},
|
||||
|
||||
@@ -155,12 +155,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Ingen tilgængelige versioner passer til {compatibilityLabel}. Vælg en version for at installere. Afhængigheder bliver ikke installeret automatisk."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installere modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Indtast modpack beskrivelse..."
|
||||
},
|
||||
@@ -170,18 +164,12 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksporter modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inkludere \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Navn"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack navn"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfigurer hvilke filer inkludere i denne eksport"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
@@ -236,9 +224,6 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Søg servere"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Er du sikker på din vil slette denne verden permanent?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Moddet"
|
||||
},
|
||||
@@ -257,15 +242,9 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Ingen servere eller verdener tilføjet"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Er du sikker på din vil fjerne {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Søg {count} verdener..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "denne server"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Indhold krævet"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,21 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen ausblenden"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Ignorieren"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Instanz öffnen"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Wiederholen"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Unbekannte Instanz"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Installationen"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Zur primären Instanz machen"
|
||||
},
|
||||
@@ -101,6 +116,12 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Spielzeit anzeigen"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Überspringt automatisch Bestätigungen mit geringem Risiko, wie z. B. die doppelte Installation von Modpacks, das Löschen normaler Inhalte, Massenaktualisierungen, das Trennen von Modpacks und Reparaturen. Warnungen bei gefährlichen Aktionen werden weiterhin angezeigt."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Nicht essenzielle Warnungen überspringen"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Ermöglicht das Umschalten der Seitenleiste."
|
||||
},
|
||||
@@ -161,12 +182,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Modpack wird installiert..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Modpaketbeschreibung eingeben..."
|
||||
},
|
||||
@@ -176,24 +191,72 @@
|
||||
"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"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpaketname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Lade Inhalte herunter"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Lade Minecraft herunter"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Lade Packdatei herunter"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Überschreibungen extrahieren"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Abschliessen"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Instanz präparieren"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Java präparieren"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Java {version} herunterladen"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Java {version} extrahieren"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Java {version} wird heruntergeladen"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Java {version} präparieren"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Java {version} validieren"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Paketmanifest wird gelesen"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Loader wird ermittelt"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Minecraft wird ermittelt"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Inhalte werden ermittelt"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Wird rückgängig gemacht"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Loader-Prozesse werden ausgeführt"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Alle Daten deiner Instanz werden permanent gelöscht, inlusive deiner Welten, Konfigurationen und allen installierten Inhalten."
|
||||
},
|
||||
@@ -218,6 +281,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instanz"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "Lade {current, number}/{total, number} Projekte..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Update abschliessen..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Versionen werden ermittelt..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "Projekt"
|
||||
},
|
||||
@@ -242,8 +314,17 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Server durchsuchen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Welt löschen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Welt löschen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Diese Welt wird permanent gelöscht von dieser Instanz, dies kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Lösche {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modifiziert"
|
||||
@@ -263,15 +344,21 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Keine Server oder Welten hinzugefügt"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Möchtest du {name} wirklich entfernen?"
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Server entfernen"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Server entfernen"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Dieser Server wird aus deiner Serverliste und aus der Serverliste im Spiel entfernt. Du kannst ihn später wieder hinzufügen, wenn du die Adresse kennst."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Entferne {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Durchsuche {count} Welten..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "dieser Server"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Inhalte benötigt"
|
||||
},
|
||||
@@ -854,6 +941,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Adresse kopieren"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Shortcut erstellen"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Nicht auf Startseite anzeigen"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,21 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen ausblenden"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Ausblenden"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Instanz öffnen"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Erneut versuchen"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Unbekannte Instanz"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Installationen"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Zur primären Instanz machen"
|
||||
},
|
||||
@@ -101,6 +116,12 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Spielzeit anzeigen"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Überspringt automatisch Bestätigungen mit geringem Risiko, wie z. B. die doppelte Installation von Modpacks, das Löschen normaler Inhalte, Massenaktualisierungen, das Trennen von Modpacks und Reparaturen. Warnungen bei gefährlichen Aktionen werden weiterhin angezeigt."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Unwichtige Warnungen überspringen"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Ermöglicht das Umschalten der Seitenleiste."
|
||||
},
|
||||
@@ -161,12 +182,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Modpack wird installiert..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Beschreibung des Modpacks eingeben..."
|
||||
},
|
||||
@@ -176,24 +191,72 @@
|
||||
"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"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpackname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Inhalte werden heruntergeladen"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Minecraft wird heruntergeladen"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Paketdatei wird herunterladen"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Überschreibungen werden extrahiert"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Wird finalisiert"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Instanz wird vorbereitet"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Java wird vorbereitet"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Java {version} wird heruntergeladen"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Java {version} wird extrahiert"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Java {version} wird heruntergeladen"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Java {version} wird vorbereitet"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Java {version} wird validiert"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Paketmanifest wird gelesen"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Loader wird ermittelt"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Minecraft wird ermittelt"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Inhalte werden ermittelt"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Wird rückgängig gemacht"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Loader-Prozesse werden ausgeführt"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Alle Daten deiner Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
|
||||
},
|
||||
@@ -218,6 +281,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instanz"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "{current, number}/{total, number} Projekte werden heruntergelanden..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Update wird fertiggestellt..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Versionen werden ermittelt..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "Projekt"
|
||||
},
|
||||
@@ -242,8 +314,17 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Server durchsuchen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Welt löschen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Welt löschen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Die Welt wird permanent von der Instanz gelöscht. Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "{name} wird gelöscht"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modifiziert"
|
||||
@@ -263,15 +344,21 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Keine Server oder Welten hinzugefügt"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Möchtest du {name} wirklich entfernen?"
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Server entfernen"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Server entfernen"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Dieser Server wird aus deiner Serverliste und aus der Serverliste im Spiel entfernt. Du kannst ihn später wieder hinzufügen, wenn du die Adresse kennst."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "{name} wird entfernt"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Durchsuche {count} Welten..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "dieser Server"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Inhalte benötigt"
|
||||
},
|
||||
@@ -854,6 +941,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Adresse kopieren"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Verknüpfung erstellen"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Nicht auf der Startseite anzeigen"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,90 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Hide more running instances"
|
||||
},
|
||||
"app.action-bar.install.copied-details": {
|
||||
"message": "Copied"
|
||||
},
|
||||
"app.action-bar.install.copy-details": {
|
||||
"message": "Copy details"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Dismiss"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Open instance"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Retry"
|
||||
},
|
||||
"app.action-bar.install.summary.app-closing": {
|
||||
"message": "Canceled due to app closing"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "Couldn't read modpack"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "Canceled"
|
||||
},
|
||||
"app.action-bar.install.summary.cleanup-incomplete": {
|
||||
"message": "Cleanup didn't finish"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "Couldn't download files"
|
||||
},
|
||||
"app.action-bar.install.summary.corrupt-download": {
|
||||
"message": "Downloaded file is corrupt"
|
||||
},
|
||||
"app.action-bar.install.summary.could-not-save-files": {
|
||||
"message": "Couldn't save files"
|
||||
},
|
||||
"app.action-bar.install.summary.download-failed": {
|
||||
"message": "Download couldn't finish"
|
||||
},
|
||||
"app.action-bar.install.summary.instance-not-found": {
|
||||
"message": "Instance couldn't be found"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-file-path": {
|
||||
"message": "File path is invalid"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack": {
|
||||
"message": "Modpack data invalid"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack-files": {
|
||||
"message": "Modpack files have invalid metadata"
|
||||
},
|
||||
"app.action-bar.install.summary.java-setup-failed": {
|
||||
"message": "Java setup couldn't finish"
|
||||
},
|
||||
"app.action-bar.install.summary.loader-setup-failed": {
|
||||
"message": "Loader setup failed"
|
||||
},
|
||||
"app.action-bar.install.summary.local-data-error": {
|
||||
"message": "Couldn't update local data"
|
||||
},
|
||||
"app.action-bar.install.summary.minecraft-setup-failed": {
|
||||
"message": "Minecraft setup failed"
|
||||
},
|
||||
"app.action-bar.install.summary.modrinth-unreachable": {
|
||||
"message": "Couldn't reach Modrinth"
|
||||
},
|
||||
"app.action-bar.install.summary.no-write-permission": {
|
||||
"message": "No permission to write"
|
||||
},
|
||||
"app.action-bar.install.summary.pack-download-failed": {
|
||||
"message": "Couldn't download pack"
|
||||
},
|
||||
"app.action-bar.install.summary.unexpected-error": {
|
||||
"message": "Something went wrong"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Unknown instance"
|
||||
},
|
||||
"app.action-bar.install.updating-shared-content": {
|
||||
"message": "Updating shared content"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Installs"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Make primary instance"
|
||||
},
|
||||
@@ -101,6 +185,12 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Show play time"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Skip non-essential warnings"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Enables the ability to toggle the sidebar."
|
||||
},
|
||||
@@ -147,7 +237,7 @@
|
||||
"message": "Discover servers"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hide already added servers"
|
||||
"message": "Hide servers already added"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
@@ -161,12 +251,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installing modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Enter modpack description..."
|
||||
},
|
||||
@@ -176,24 +260,102 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Export modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Include \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Name"
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configure which files are included in this export"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Version number"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Downloading content"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Downloading Minecraft"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Downloading pack file"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Extracting overrides"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Finalizing"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Queued to install"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Preparing Java"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Downloading Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Extracting Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Fetching Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Preparing Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Validating Java {version}"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Reading pack manifest"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Resolving loader"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Resolving Minecraft"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Resolving content"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Rolling back"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Running loader processors"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.added-label": {
|
||||
"message": "Added"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.changes-body": {
|
||||
"message": "Your local instance is ahead of the users you've shared it with."
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.changes-header": {
|
||||
"message": "Your changes haven't been shared yet"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.publish-button": {
|
||||
"message": "Push update"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.publishing-button": {
|
||||
"message": "Pushing..."
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.removed-label": {
|
||||
"message": "Removed"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.review-admonition-header": {
|
||||
"message": "Push update"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.review-description": {
|
||||
"message": "Review the content changes that will be shared with everyone using this instance."
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.review-header": {
|
||||
"message": "Review changes"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.reviewing-button": {
|
||||
"message": "Reviewing..."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "All data for your instance will be permanently deleted, including your worlds, configs, and all installed content."
|
||||
},
|
||||
@@ -218,6 +380,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instance"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "Downloading {current, number}/{total, number} projects..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Finishing update..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Resolving versions..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "project"
|
||||
},
|
||||
@@ -236,17 +407,107 @@
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Successfully uploaded"
|
||||
},
|
||||
"app.instance.share.empty.description": {
|
||||
"message": "You can share this instance with your friends!"
|
||||
},
|
||||
"app.instance.share.empty.heading": {
|
||||
"message": "No friends invited"
|
||||
},
|
||||
"app.instance.share.empty.invite-friends-button": {
|
||||
"message": "Invite friends"
|
||||
},
|
||||
"app.instance.share.invite-modal.heading": {
|
||||
"message": "Share {name}"
|
||||
},
|
||||
"app.instance.share.locked.empty-description-prefix": {
|
||||
"message": "You need to sign in as"
|
||||
},
|
||||
"app.instance.share.locked.empty-description-suffix": {
|
||||
"message": "to access this page."
|
||||
},
|
||||
"app.instance.share.locked.linked-account-fallback": {
|
||||
"message": "the linked account"
|
||||
},
|
||||
"app.instance.share.locked.signed-out-heading": {
|
||||
"message": "Not signed in"
|
||||
},
|
||||
"app.instance.share.locked.switch-account-button": {
|
||||
"message": "Switch account"
|
||||
},
|
||||
"app.instance.share.locked.wrong-account-heading": {
|
||||
"message": "Wrong account"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.effect-access": {
|
||||
"message": "They will no longer receive updates for this shared instance"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.effect-installed-copy": {
|
||||
"message": "Any copy they already installed will stay on their device"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.effect-invite-again": {
|
||||
"message": "You can invite them again later"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.effect-last-user": {
|
||||
"message": "If this is the last user, sharing will be turned off for this instance"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.effects-label": {
|
||||
"message": "What happens?"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.header": {
|
||||
"message": "Revoke access"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.remove-button": {
|
||||
"message": "Revoke access"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.user-avatar-alt": {
|
||||
"message": "{username}'s avatar"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.warning-body": {
|
||||
"message": "If you revoke {username}'s access to this shared instance, you'll need to invite them again before they can receive updates."
|
||||
},
|
||||
"app.instance.share.sign-in.button": {
|
||||
"message": "Sign in"
|
||||
},
|
||||
"app.instance.share.unlink.body": {
|
||||
"message": "You must unlink this modpack to share your instance"
|
||||
},
|
||||
"app.instance.share.unlink.header": {
|
||||
"message": "Sharing requires unlinking"
|
||||
},
|
||||
"app.instance.shared-instance-wrong-account.fallback-username": {
|
||||
"message": "the linked account"
|
||||
},
|
||||
"app.instance.shared-instance-wrong-account.owner-admonition-body-v2": {
|
||||
"message": "to manage this shared instance. You won't be able to push updates to users."
|
||||
},
|
||||
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
|
||||
"message": "Sign in as"
|
||||
},
|
||||
"app.instance.shared-instance-wrong-account.signed-out-header": {
|
||||
"message": "You need to sign in to Modrinth"
|
||||
},
|
||||
"app.instance.shared-instance-wrong-account.user-admonition-body-v2": {
|
||||
"message": "to receive updates for this shared instance."
|
||||
},
|
||||
"app.instance.shared-instance-wrong-account.warning-header": {
|
||||
"message": "You are using the wrong Modrinth account"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Add server"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Browse servers"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' will be **permanently deleted**, and there will be no way to recover it."
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Delete world"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Are you sure you want to permanently delete this world?"
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Delete world"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "This world will be permanently deleted from this instance. This action cannot be undone."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Deleting {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modded"
|
||||
@@ -266,24 +527,27 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "No servers or worlds added"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' will be removed from your list, including in-game, and there will be no way to recover it."
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Remove server"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) will be removed from your list, including in-game, and there will be no way to recover it."
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Remove server"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Are you sure you want to remove {name}?"
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "This server will be removed from your server list and from the in-game server list. You can add it again later if you know the address."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Removing {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Search {count} worlds..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "this server"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Content required"
|
||||
},
|
||||
"app.modal.install-to-play.external-file-warning": {
|
||||
"message": "{count, plural, one {This instance includes # file that is not from Modrinth.} other {This instance includes # files that are not from Modrinth.}}"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Install to play"
|
||||
},
|
||||
@@ -302,30 +566,60 @@
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Shared instance"
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance-content": {
|
||||
"message": "Shared instance content"
|
||||
},
|
||||
"app.modal.install-to-play.shared-server-instance": {
|
||||
"message": "Shared server instance"
|
||||
},
|
||||
"app.modal.install-to-play.trust-warning-description": {
|
||||
"message": "A shared instance will install files on your computer and may include content not from Modrinth."
|
||||
},
|
||||
"app.modal.install-to-play.trust-warning-header": {
|
||||
"message": "Do you trust this user?"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "View contents"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Update to play"
|
||||
},
|
||||
"app.modal.update-to-play.shared-instance-added-label": {
|
||||
"message": "Added"
|
||||
},
|
||||
"app.modal.update-to-play.shared-instance-removed-label": {
|
||||
"message": "Removed"
|
||||
},
|
||||
"app.modal.update-to-play.update-required": {
|
||||
"message": "Update required"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "An update is required to play {name}. Please update to the latest version to launch the game."
|
||||
"message": "An update is required to play {name}. Please update to latest version to launch the game."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "This project is already installed"
|
||||
},
|
||||
"app.project.install-button.switch-version": {
|
||||
"message": "Switch version"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Back to discover"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "All versions"
|
||||
},
|
||||
"app.project.version.download-in-browser": {
|
||||
"message": "Download in browser"
|
||||
},
|
||||
"app.project.version.installing": {
|
||||
"message": "Installing"
|
||||
},
|
||||
"app.project.versions.already-installed": {
|
||||
"message": "Already installed"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Developer mode enabled."
|
||||
},
|
||||
@@ -446,6 +740,9 @@
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chaos-cubed": {
|
||||
"message": "Chaos Cubed"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
@@ -545,6 +842,39 @@
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} online"
|
||||
},
|
||||
"content.shared-instance.change-version-body": {
|
||||
"message": "Changing the version only changes your local copy. Future shared instance updates may restore or change it again."
|
||||
},
|
||||
"content.shared-instance.delete-bulk-body": {
|
||||
"message": "Some selected projects are part of the shared instance. Deleting them only changes your local copy, and future shared instance updates may restore or change them again."
|
||||
},
|
||||
"content.shared-instance.delete-button": {
|
||||
"message": "Delete anyway"
|
||||
},
|
||||
"content.shared-instance.delete-many-button": {
|
||||
"message": "Delete {count, number} projects anyway"
|
||||
},
|
||||
"content.shared-instance.delete-single-body": {
|
||||
"message": "Deleting it only changes your local copy. Future shared instance updates may restore or change it again."
|
||||
},
|
||||
"content.shared-instance.disable-bulk-body": {
|
||||
"message": "Some selected projects are part of the shared instance. Disabling them only changes your local copy, and future shared instance updates may re-enable, restore, or change them again."
|
||||
},
|
||||
"content.shared-instance.disable-button": {
|
||||
"message": "Disable anyway"
|
||||
},
|
||||
"content.shared-instance.disable-many-button": {
|
||||
"message": "Disable {count, number} projects anyway"
|
||||
},
|
||||
"content.shared-instance.disable-single-body": {
|
||||
"message": "Disabling it only changes your local copy. Future shared instance updates may re-enable, restore, or change it again."
|
||||
},
|
||||
"content.shared-instance.unlink-body": {
|
||||
"message": "Unlinking only changes your local copy. Future shared instance updates may restore or change it again."
|
||||
},
|
||||
"content.shared-instance.warning-header": {
|
||||
"message": "This is part of the shared instance"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Add a friend"
|
||||
},
|
||||
@@ -608,6 +938,48 @@
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Sign in to a Modrinth account</link> to add friends and see what they're playing!"
|
||||
},
|
||||
"installation-settings.shared-instance.linked-title": {
|
||||
"message": "Linked shared instance"
|
||||
},
|
||||
"installation-settings.shared-instance.title": {
|
||||
"message": "Shared instance"
|
||||
},
|
||||
"installation-settings.shared-instance.unlink-button": {
|
||||
"message": "Unlink shared instance"
|
||||
},
|
||||
"installation-settings.shared-instance.unlink-description": {
|
||||
"message": "Disconnect this local instance from future shared updates."
|
||||
},
|
||||
"installation-settings.shared-instance.unlinking-button": {
|
||||
"message": "Unlinking..."
|
||||
},
|
||||
"installation-settings.shared-instance.unpublish-button": {
|
||||
"message": "Unpublish shared instance"
|
||||
},
|
||||
"installation-settings.shared-instance.unpublish-description": {
|
||||
"message": "Stop sharing this instance and remove it from Modrinth."
|
||||
},
|
||||
"installation-settings.shared-instance.unpublishing-button": {
|
||||
"message": "Unpublishing..."
|
||||
},
|
||||
"installation-settings.unlink-shared-instance.modal.admonition-body": {
|
||||
"message": "This only affects your local instance. Your installed content will stay on this device, and the shared instance and other people using it will not be affected."
|
||||
},
|
||||
"installation-settings.unlink-shared-instance.modal.admonition-header": {
|
||||
"message": "Unlinking shared instance"
|
||||
},
|
||||
"installation-settings.unlink-shared-instance.modal.header": {
|
||||
"message": "Unlink shared instance"
|
||||
},
|
||||
"installation-settings.unpublish-shared-instance.modal.admonition-body": {
|
||||
"message": "This deletes the shared instance from Modrinth's servers. People using it in the Modrinth App will stop receiving updates, but your local instance and its content will stay on this device."
|
||||
},
|
||||
"installation-settings.unpublish-shared-instance.modal.admonition-header": {
|
||||
"message": "Unpublishing shared instance"
|
||||
},
|
||||
"installation-settings.unpublish-shared-instance.modal.header": {
|
||||
"message": "Unpublish shared instance"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Add and play"
|
||||
},
|
||||
@@ -854,6 +1226,30 @@
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "Enter width..."
|
||||
},
|
||||
"instance.shared-instance.error.title": {
|
||||
"message": "Something has gone wrong"
|
||||
},
|
||||
"instance.shared-instance.owner-tooltip": {
|
||||
"message": "This instance's content is being shared to other users."
|
||||
},
|
||||
"instance.shared-instance.tooltip": {
|
||||
"message": "This instance's content is being managed by someone else."
|
||||
},
|
||||
"instance.shared-instance.unavailable.access-revoked-text-v2": {
|
||||
"message": "Your access to the shared instance was revoked. Your local instance is still available, but it is no longer linked and won't receive updates."
|
||||
},
|
||||
"instance.shared-instance.unavailable.deleted-text-v2": {
|
||||
"message": "The shared instance was deleted. Your local instance is still available, but it is no longer linked and won't receive updates."
|
||||
},
|
||||
"instance.shared-instance.unavailable.manager-fallback": {
|
||||
"message": "the instance manager"
|
||||
},
|
||||
"instance.shared-instance.unavailable.text-v2": {
|
||||
"message": "Your local instance is still available, but it is no longer linked and won't receive updates."
|
||||
},
|
||||
"instance.shared-instance.unavailable.title": {
|
||||
"message": "Shared instance no longer available"
|
||||
},
|
||||
"instance.worlds.a_minecraft_server": {
|
||||
"message": "A Minecraft Server"
|
||||
},
|
||||
@@ -863,6 +1259,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Copy address"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Create shortcut"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Don't show on Home"
|
||||
},
|
||||
@@ -914,6 +1313,24 @@
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Sign in to Minecraft"
|
||||
},
|
||||
"modal.modrinth-account-required.create-account-button": {
|
||||
"message": "Create an account"
|
||||
},
|
||||
"modal.modrinth-account-required.description": {
|
||||
"message": "You'll need to sign into your Modrinth account before you can use this feature."
|
||||
},
|
||||
"modal.modrinth-account-required.header": {
|
||||
"message": "Account required"
|
||||
},
|
||||
"modal.modrinth-account-required.sign-in-button": {
|
||||
"message": "Sign in to Modrinth"
|
||||
},
|
||||
"modal.modrinth-account-required.sign-in-heading": {
|
||||
"message": "Sign in to a Modrinth account"
|
||||
},
|
||||
"modal.modrinth-account-required.support-prompt": {
|
||||
"message": "Having trouble signing in? <support>Get support</support>"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Provided by the instance"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,21 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ocultar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Descartar"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Abrir instancia"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Reintentar"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Instancia desconocida"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Instalaciones"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Hacer instancia principal"
|
||||
},
|
||||
@@ -101,6 +116,12 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Mostrar tiempo de juego"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Omite automáticamente las confirmaciones de bajo riesgo, como las instalaciones duplicadas de paquetes de mods, la eliminación de contenido normal, las actualizaciones masivas, la desvinculación de paquetes de mods y las solicitudes de reparación. Las advertencias peligrosas seguirán mostrándose."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Omitir advertencias que no sean esenciales"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Permite mostrar u ocultar la barra lateral."
|
||||
},
|
||||
@@ -161,12 +182,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalando modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Introduce la descripción del modpack..."
|
||||
},
|
||||
@@ -176,24 +191,72 @@
|
||||
"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"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configura que archivos incluir en esta exportación"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número de la versión"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Descargando contenido"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Descargando Minecraft"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Descargando el archivo del paquete"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Extracción de anulaciones"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Finalizando"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Preparando la instancia"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Preparando Java"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Descargando Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Descomprimiendo Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Obteniendo Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Preparando Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Validación de Java {version}"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Leer el manifiesto del paquete"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Resolviendo el cargador"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Resolviendo Minecraft"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Resolviendo el contenido"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Revertir"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Ejecutando los procesadores del cargador"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Todos los datos de tu instancia se eliminarán permanentemente, incluidos tus mundos, configuraciones y todo el contenido instalado."
|
||||
},
|
||||
@@ -218,6 +281,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instancia"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "Descargando {current, number}/{total, number} proyectos..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Finalizando la actualización..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Resolviendo versiones..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "proyecto"
|
||||
},
|
||||
@@ -242,8 +314,17 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Explorar servidores"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "¿Estás seguro de que quieres eliminar este mundo de forma permanente?"
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Eliminar mundo"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Eliminar mundo"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Este mundo se eliminará de forma permanente de esta instancia. Esta acción no se puede deshacer."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Eliminando {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Con mods"
|
||||
@@ -263,15 +344,21 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "No hay servidores ni mundos añadidos"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "¿Estás seguro de que quieres eliminar {name}?"
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Eliminar servidor"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Eliminar servidor"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Este servidor se eliminará de tu lista de servidores y de la lista de servidores del juego. Puedes volver a agregarlo más adelante si conoces la dirección."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Se está eliminando {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Buscar en {count} mundos..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "este servidor"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Contenido requerido"
|
||||
},
|
||||
@@ -854,6 +941,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Copiar dirección"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Crear un acceso directo"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "No mostrar en la página de inicio"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ocultar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Descartar"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Establecer como instancia principal"
|
||||
},
|
||||
@@ -161,12 +164,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalando modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Escribe la descripción del modpack..."
|
||||
},
|
||||
@@ -176,18 +173,12 @@
|
||||
"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"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configura que archivos se incluirán en esta instancia"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número de versión"
|
||||
},
|
||||
@@ -242,12 +233,6 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Explorar servidores"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' será **permanentemente eliminado**, y habrá forma de recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "¿Estás seguro de qué quieres eliminar permanentemente este mundo?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modeado"
|
||||
},
|
||||
@@ -266,21 +251,9 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "No hay servidores o mundos añadidos"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "¿Estás seguro de qué quieres eliminar {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Buscar {count} mundos..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "este servidor"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Contenido obligatorio"
|
||||
},
|
||||
|
||||
@@ -2,12 +2,30 @@
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Ladataan Java-versiota {version}"
|
||||
},
|
||||
"app.action-bar.downloading-update": {
|
||||
"message": "Ladataan päivitystä"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Lataukset"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Piilota enemmän käynnissä olevia instansseja"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Hylkää"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Avaa instanssi"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Yritä uudelleen"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Tuntematon instanssi"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Asennukset"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Aseta ensisijaiseksi instanssiksi"
|
||||
},
|
||||
@@ -20,12 +38,18 @@
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Ensisijainen instanssi"
|
||||
},
|
||||
"app.action-bar.reload-to-update": {
|
||||
"message": "Lataa uudelleen päivittääksesi"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Näytä enemmän käynnissä olevia instansseja"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Pysäytä instanssi"
|
||||
},
|
||||
"app.action-bar.update": {
|
||||
"message": "Päivitä"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Näytä aktiiviset lataukset"
|
||||
},
|
||||
@@ -86,6 +110,18 @@
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Valitse vaihtoehto"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Näyttää, kuinka paljon aikaa olet käyttänyt instanssin pelaamiseen."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Näyttää peliaika"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Ohittaa automaattisesti vähäriskiset vahvistukset, kuten modpackien kaksoisasennukset, tavallisen sisällön poistamisen, massapäivitykset, modpackien linkityksen poistamisen ja korjauskehotteet. Vaaralliset varoitukset näytetään silti."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Ohita ei olennaiset varoitukset"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Laittaa päälle mahdollisuuden kytkeä sivupalkkia."
|
||||
},
|
||||
@@ -146,12 +182,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Yhtään versiota ei ole saatavilla yhteensopivuudella {compatibilityLabel}. Valitse asennettava versio siitä huolimatta. Riippuvuuksia ei asenneta automaattisesti."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Asennetaan modipakettia..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Lisää modipaketin kuvaus..."
|
||||
},
|
||||
@@ -161,24 +191,72 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Vie modipaketti"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sisällytä \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versio numero"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Ladataan sisältöä"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Ladataan Minecraftia"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Ladataan paketti tiedostoja"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Puretaan ohituksia"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Viimeistellään"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Valmistellaan instanssia"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Valmistellaan Javaa"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Ladataan Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Puretaan Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Haetaan Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Valmistellaan Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Tarkistetaan Java {version}"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Luetaan paketin manifestia"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Ratkaistaan lataajaa"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Ratkaistaan Minecraftia"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Ratkaistaan sisältöä"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Palautetaan muutoksia"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Suoritetaan lataajan käsittelijöitä"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Kaikki sinun instanssisi data poistetaan pysyvästi, sisältäen maailmasi, konfiguraatiosi ja asennetun sisällön."
|
||||
},
|
||||
@@ -203,6 +281,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instanssi"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "Ladataan {current, number}/{total, number} projekteja..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Viimeistellään päivitystä..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Selvitetään versioita..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projekti"
|
||||
},
|
||||
@@ -227,11 +314,17 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Selaa palvelimia"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' tullaan **poistamaan pysyvästi**, eikä tule olemaan mitään tapaa palauttaa sitä."
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Poista maailma"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Oletko varma että haluat pysyvästi poistaa tämän maailman?"
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Poista maailma"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Tämä maailma poistetaan pysyvästi tästä instanssista. Tätä toimintoa ei voi kumota."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Poistetaan {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modattu"
|
||||
@@ -251,21 +344,21 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Ei palvelimia tai maailmoja lisättynä"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' tullaan poistamaan sinun listaltasi, mukaan lukien pelin sisällä, eikä tule olemaan mitään tapaa palauttaa sitä."
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Poista palvelin"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) tullaan poistamaan sinun listaltasi, mukaan lukien pelin sisällä, eikä tule olemaan mitään tapaa palauttaa sitä."
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Poista palvelin"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Oletko varma että haluat poistaa {name}?"
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Tämä palvelin poistetaan palvelinluettelostasi ja pelin sisäisestä palvelinluettelosta. Voit lisätä sen myöhemmin uudelleen, jos tiedät osoitteen."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Poistetaan {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Hae {count} maailmasta..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "tämän palvelimen"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Sisältö vaaditaan"
|
||||
},
|
||||
@@ -335,15 +428,147 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Resurssien hallinta"
|
||||
},
|
||||
"app.skins.add-button": {
|
||||
"message": "Lisää ulkoasu"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Raahaa ja pudota"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Käytä"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Poista ulkoasu"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Tämä poistaa valitun skinin pysyvästi. Tätä toimintoa ei voi kumota."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "Haluatko varmasti poistaa ulkoasun?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Tiedoston lukeminen epäonnistui."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Virhe prosessoidessa tiedostoa"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Muokkaa ulkoasua"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Lisää ulkoasu"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Lisätään ulkoasua"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Käsien tyyli"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Ohut"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Paksu"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Viitta"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Viitta"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Muokataan ulkoasua"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "Tee muokkaus ulkoasuun ensin!"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Ei viittaa"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Ei mitään"
|
||||
},
|
||||
"app.skins.modal.replace-texture-button": {
|
||||
"message": "Korvaa tekstuuri"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Tallenna ulkoasu"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Tallennetaan..."
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Tekstuuri"
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "Lataa ulkoasu ensin!"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Muokkaa ulkoasua"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Esikatsellaan"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "Vaihdat ulkoasua liian usein. Mojangin palvelimet ovat estäneet uudet pyynnöt väliaikaisesti. Yritä hetken kuluttua uudelleen."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "Hidasta vähän!"
|
||||
},
|
||||
"app.skins.reorder-error.text": {
|
||||
"message": "Ulkoasujen järjestystä ei voitu tallentaa."
|
||||
},
|
||||
"app.skins.reorder-error.title": {
|
||||
"message": "Skinien järjestyksen muuttaminen epäonnistui"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Oletus ulkoasut"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "Sait nämä skinit lahjoittamalla Modrinthin Pride-keräykseen Pride-kuukauden aikana."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Sekasorron ratsut"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Tallennetut ulkoasut"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "The Copper Age"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "The Garden Awakens"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Pienten vallankumous"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Kirjaudu sisään"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Kirjaudu Minecraft-tilillesi käyttääksesi Modrinth-sovelluksen ulkoasujen hallintaominaisuuksia."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Innostunut Modrinth Botti"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Kirjaudu sisään"
|
||||
},
|
||||
@@ -569,15 +794,30 @@
|
||||
"instance.settings.tabs.general.name": {
|
||||
"message": "Nimi"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel": {
|
||||
"message": "Päivitys kanava"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.alpha": {
|
||||
"message": "Alfa"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.alpha.description": {
|
||||
"message": "Release-, beta- ja alpha-versiot näytetään saatavilla olevina päivityksinä."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.beta.description": {
|
||||
"message": "Release- ja beta-versiot näytetään saatavilla olevina päivityksinä."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.release": {
|
||||
"message": "Julkaisu"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.release.description": {
|
||||
"message": "Vain release-versiot näytetään saatavilla olevina päivityksinä."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.select": {
|
||||
"message": "Valitse päivitys kanava"
|
||||
},
|
||||
"instance.settings.tabs.hooks": {
|
||||
"message": "Aloitustoiminnot"
|
||||
},
|
||||
@@ -701,6 +941,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Kopioi osoite"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Luo pikakuvake"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Älä näytä koti näkymässä"
|
||||
},
|
||||
|
||||
@@ -155,12 +155,6 @@
|
||||
"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..."
|
||||
},
|
||||
@@ -170,18 +164,12 @@
|
||||
"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"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Pangalan ng modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numero ng bersiyon"
|
||||
},
|
||||
@@ -236,12 +224,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -260,21 +242,9 @@
|
||||
"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}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Hanapin sa {count} mundo..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "server na ito"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Nangangailangan ng kontento"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,21 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Masquer plus d'instances en exécution"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Rejeter"
|
||||
},
|
||||
"app.action-bar.install.open-instance": {
|
||||
"message": "Ouvrir l'instance"
|
||||
},
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Réessayer"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Instance inconnue"
|
||||
},
|
||||
"app.action-bar.installs": {
|
||||
"message": "Installations"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Définir en tante qu'instance première"
|
||||
},
|
||||
@@ -101,6 +116,12 @@
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Afficher le temps de jeu"
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.description": {
|
||||
"message": "Ignore automatiquement les confirmations à faible risque, comme l'installation de modpacks en double, la suppression de contenu standard, les mises à jour groupées, la dissociation des modpacks et les demandes de réparation. Les avertissements concernant les actions dangereuses continueront d'être affichés."
|
||||
},
|
||||
"app.appearance-settings.skip-non-essential-warnings.title": {
|
||||
"message": "Cacher les alertes non essentielles"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Permet d'ouvrir ou de fermer la barre latérale."
|
||||
},
|
||||
@@ -161,12 +182,6 @@
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Aucune version disponible ne correspond à {compatibilityLabel}. Sélectionnez une version pour l'installer malgré tout. Les dépendances ne seront pas installées automatiquement."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installation du modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Saisir la description du modpack..."
|
||||
},
|
||||
@@ -176,24 +191,72 @@
|
||||
"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"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Nom du modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Configurez quels fichiers sont inclus dans cette exportation"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numéro de version"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.install.phase.downloading_content": {
|
||||
"message": "Téléchargement du contenu"
|
||||
},
|
||||
"app.install.phase.downloading_minecraft": {
|
||||
"message": "Téléchargement de Minecraft"
|
||||
},
|
||||
"app.install.phase.downloading_pack_file": {
|
||||
"message": "Téléchargement du fichier de pack"
|
||||
},
|
||||
"app.install.phase.extracting_overrides": {
|
||||
"message": "Extraction des fichiers personnalisés"
|
||||
},
|
||||
"app.install.phase.finalizing": {
|
||||
"message": "Finalisation"
|
||||
},
|
||||
"app.install.phase.preparing_instance": {
|
||||
"message": "Préparation de l'instance"
|
||||
},
|
||||
"app.install.phase.preparing_java": {
|
||||
"message": "Préparation de Java"
|
||||
},
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Téléchargement de Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.extracting": {
|
||||
"message": "Extraction de Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.fetching-metadata": {
|
||||
"message": "Récupération de Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.resolving": {
|
||||
"message": "Préparation de Java {version}"
|
||||
},
|
||||
"app.install.phase.preparing_java.validating": {
|
||||
"message": "Validation de Java {version}"
|
||||
},
|
||||
"app.install.phase.reading_pack_manifest": {
|
||||
"message": "Lecture du manifeste du pack"
|
||||
},
|
||||
"app.install.phase.resolving_loader": {
|
||||
"message": "Résolution du loader"
|
||||
},
|
||||
"app.install.phase.resolving_minecraft": {
|
||||
"message": "Résolution de Minecraft"
|
||||
},
|
||||
"app.install.phase.resolving_pack": {
|
||||
"message": "Résolution du contenu"
|
||||
},
|
||||
"app.install.phase.rolling_back": {
|
||||
"message": "Annulation"
|
||||
},
|
||||
"app.install.phase.running_loader_processors": {
|
||||
"message": "Exécution des processeurs du chargeur"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Toutes les données pour votre instance seront supprimées à jamais, y compris vos mondes, vos configurations, et le contenu installé."
|
||||
},
|
||||
@@ -218,6 +281,15 @@
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instance"
|
||||
},
|
||||
"app.instance.mods.bulk-update.downloading-projects": {
|
||||
"message": "Téléchargement de {current, number}/{total, number} projets..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.finishing": {
|
||||
"message": "Finalisation de la mise à jour..."
|
||||
},
|
||||
"app.instance.mods.bulk-update.resolving-versions": {
|
||||
"message": "Résolution des versions..."
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projet"
|
||||
},
|
||||
@@ -242,8 +314,17 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Parcourir des serveurs"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Êtes-vous sûr·e de vouloir supprimer ce monde pour toujours ?"
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Supprimer le monde"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Supprimer le monde"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-body": {
|
||||
"message": "Ce monde sera définitivement supprimé de cette instance. Cette action est irréversible."
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Suppression de {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Moddé"
|
||||
@@ -263,15 +344,21 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Aucun serveur ou monde ajoutés"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Êtes-vous sûr·e de vouloir retirer {name} ?"
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Enlever le serveur"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Enlever le serveur"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-body": {
|
||||
"message": "Ce serveur sera enlevé de votre liste de serveur et de la liste de serveur en jeu. Vous pourrez le rajouter plus tard si vous connaissez son adresse."
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Retrait de {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Rechercher {count} mondes..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "ce serveur"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Contenu requis"
|
||||
},
|
||||
@@ -851,6 +938,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Copier l'adresse"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Créer un raccourci"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Ne pas montrer dans l'Accueil"
|
||||
},
|
||||
|
||||
@@ -119,12 +119,6 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "עיון בשרתים"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' יימחק לצמיתות, ולא תהיה דרך לשחזר אותו."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "האם אתה בטוח שברצונך למחוק לצמיתות את העולם הזה?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "עם מודים"
|
||||
},
|
||||
@@ -143,21 +137,9 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "אין שרתים אן עולמות שנוספו"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' יוסר מהרשימה שלך, כולל מתוך המשחק, ולא תהיה דרך לשחזר אותו."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) יוסר מהרשימה שלך, כולל מתוך המשחק, ולא תהיה דרך לשחזר אותו."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "אתה בטוח שאתה רוצה להסיר {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "חיפוש ב-{count} עולמות..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "השרת הזה"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "תוכן נדרש"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user