mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
490
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
003b5249db | ||
|
|
2a97f642d2 | ||
|
|
238ffbdf5b | ||
|
|
9a8712c76e | ||
|
|
f62c60a681 | ||
|
|
9b2f0c88cd | ||
|
|
01b8ee6909 | ||
|
|
5a51a755eb | ||
|
|
4cfac2c8a2 | ||
|
|
f6fcdd336f | ||
|
|
5594771ad8 | ||
|
|
5d04992a28 | ||
|
|
c9c8079853 | ||
|
|
97051cc64d | ||
|
|
0a04478149 | ||
|
|
789ec8966c | ||
|
|
51a83b4536 | ||
|
|
73abe272d1 | ||
|
|
4a0c610fc5 | ||
|
|
913dee9090 | ||
|
|
43eb53eda5 | ||
|
|
c381adff85 | ||
|
|
ace2659861 | ||
|
|
507d03eeba | ||
|
|
d4932d3089 | ||
|
|
4b6de7526c | ||
|
|
b95e4ced22 | ||
|
|
200b4f56c6 | ||
|
|
83d53dafe7 | ||
|
|
98175a58a6 | ||
|
|
20cbe1ad8f | ||
|
|
9d5d34fde8 | ||
|
|
2d5c26896f | ||
|
|
ea3bb334a8 | ||
|
|
024e079a7d | ||
|
|
d902b281f7 | ||
|
|
c4a0008708 | ||
|
|
835f80ee50 | ||
|
|
155f4091a6 | ||
|
|
e1ee9c364b | ||
|
|
0029a22569 | ||
|
|
211ec20970 | ||
|
|
34997bada5 | ||
|
|
63daac917b | ||
|
|
51ceb9d851 | ||
|
|
51066c476a | ||
|
|
47ef7ee42e | ||
|
|
6fba33d443 | ||
|
|
017f6a5afb | ||
|
|
1ab722411a | ||
|
|
45387e5fb6 | ||
|
|
e362de45fb | ||
|
|
bacc10d2f5 | ||
|
|
5b49af1fe8 | ||
|
|
f052ecd702 | ||
|
|
33ff2a0759 | ||
|
|
ec81bcb13c | ||
|
|
b6b4bc21f1 | ||
|
|
9a83db2e67 | ||
|
|
30c48718e2 | ||
|
|
8328a0d61a | ||
|
|
b62bc6f3b8 | ||
|
|
0e752ab415 | ||
|
|
9f558404bd | ||
|
|
4be2f77bb0 | ||
|
|
b3fbd884e0 | ||
|
|
8c0edf669d | ||
|
|
f01c901445 | ||
|
|
2a91fc31f1 | ||
|
|
d1e4c1039f | ||
|
|
9432d6d5e8 | ||
|
|
c053c00bd7 | ||
|
|
9d0df74475 | ||
|
|
09e989a4c4 | ||
|
|
d4ef5f36c3 | ||
|
|
a9e0655859 | ||
|
|
e7eb4899a1 | ||
|
|
76ba11d966 | ||
|
|
f22e49e4f5 | ||
|
|
3c1bd86dcc | ||
|
|
4adbd0b843 | ||
|
|
cec35dcb60 | ||
|
|
a536d795f3 | ||
|
|
e3e04931cf | ||
|
|
3e505e0d96 | ||
|
|
dd4b054d95 | ||
|
|
e80d7730ca | ||
|
|
0facf26b04 | ||
|
|
37eac92329 | ||
|
|
90438a1ad5 | ||
|
|
e962521492 | ||
|
|
45a397d52b | ||
|
|
d9d7750781 | ||
|
|
1101e71fdd | ||
|
|
34b4ae283e | ||
|
|
a8c5e036d0 | ||
|
|
4eb0f0c206 | ||
|
|
a3bc35c303 | ||
|
|
57e012f9b7 | ||
|
|
8ab1895d8a | ||
|
|
13e5529f00 | ||
|
|
428efde36d | ||
|
|
a978873bff | ||
|
|
b005c1f522 | ||
|
|
b6c22d6ca6 | ||
|
|
50064c4ed6 | ||
|
|
48248eafdc | ||
|
|
b34564a770 | ||
|
|
d713cea180 | ||
|
|
695233e736 | ||
|
|
8789d7b057 | ||
|
|
510ea6cde4 | ||
|
|
b1954be2c7 | ||
|
|
9105a68923 | ||
|
|
06e2f59a94 | ||
|
|
ddb013e024 | ||
|
|
3f5e3b1d8b | ||
|
|
323090966b | ||
|
|
16204d30f8 | ||
|
|
34cbc7e0c1 | ||
|
|
5d6593a9da | ||
|
|
ab753a82bc | ||
|
|
880e759336 | ||
|
|
84b386141c | ||
|
|
e9a4cc60ca | ||
|
|
d5869c514e | ||
|
|
de0a03b2e9 | ||
|
|
3f6c79b00d | ||
|
|
56c8bb1950 | ||
|
|
f81f951814 | ||
|
|
345ada27c0 | ||
|
|
e3395a7366 | ||
|
|
3552c8280b | ||
|
|
11f00be606 | ||
|
|
a207daef0d | ||
|
|
ddd395ed4f | ||
|
|
04a7ae55e7 | ||
|
|
5a33d462f6 | ||
|
|
cefa7b90ed | ||
|
|
0875a8a0bc | ||
|
|
5624e86c2b | ||
|
|
c622a9281f | ||
|
|
937d385241 | ||
|
|
3ec9bcf7e7 | ||
|
|
3dd5314062 | ||
|
|
d77823c5f4 | ||
|
|
41b48bd353 | ||
|
|
6225ece6be | ||
|
|
3f8805b953 | ||
|
|
6d68c0983f | ||
|
|
3ac0eb896c | ||
|
|
9c309e6da2 | ||
|
|
deaa57fa15 | ||
|
|
8053fec29f | ||
|
|
78aca7e5c0 | ||
|
|
728f8db7b9 | ||
|
|
4c14339b4b | ||
|
|
16ac2aae6b | ||
|
|
6d68d50699 | ||
|
|
400c571fe6 | ||
|
|
e57c15b3ce | ||
|
|
7cb7e881fa | ||
|
|
2cf82349a5 | ||
|
|
638bb55649 | ||
|
|
6d1b0eef15 | ||
|
|
03658b6a62 | ||
|
|
f998a8dca5 | ||
|
|
0d6bee3a5b | ||
|
|
7ce334aeb2 | ||
|
|
fe3a1b2058 | ||
|
|
662b73211a | ||
|
|
c453a03a02 | ||
|
|
05f8113bd0 | ||
|
|
63dfc3f636 | ||
|
|
c96a303d8a | ||
|
|
2d7e87a4cb | ||
|
|
fa421b4b83 | ||
|
|
79217e78b4 | ||
|
|
bdd808c279 | ||
|
|
986a7e6216 | ||
|
|
b54fcaa0b1 | ||
|
|
1cf782c298 | ||
|
|
fb1050e409 | ||
|
|
5c29a8c7dd | ||
|
|
09dead50d2 | ||
|
|
772e0ee220 | ||
|
|
86b0de3cee | ||
|
|
d174d96b74 | ||
|
|
1d193ed01b | ||
|
|
adf365d99d | ||
|
|
f3f48c3c6f | ||
|
|
e072f2237b | ||
|
|
306eee3a21 | ||
|
|
ca1d66d070 | ||
|
|
7e1400d111 | ||
|
|
7595e77170 | ||
|
|
08fcc61d35 | ||
|
|
b36801c5ed | ||
|
|
4ed1a1ae7f | ||
|
|
04db01cb55 | ||
|
|
8f5185de1c | ||
|
|
9f6db31785 | ||
|
|
a869086ce9 | ||
|
|
2af6a1b36f | ||
|
|
d4381f513f | ||
|
|
a281f13f15 | ||
|
|
a9641dadff | ||
|
|
c94dde9b47 | ||
|
|
976644d1e6 | ||
|
|
11fe90a69b | ||
|
|
1108086854 | ||
|
|
5aea892a39 | ||
|
|
3f3e6f5199 | ||
|
|
2efcd383bb | ||
|
|
faec9c2965 | ||
|
|
a0e8c7f924 | ||
|
|
6efdfdf17e | ||
|
|
aec268c6e9 | ||
|
|
2c096a85d6 | ||
|
|
240e5455cc | ||
|
|
c538a9ec6d | ||
|
|
72458f5c41 | ||
|
|
82e4eb7b40 | ||
|
|
c9bfc4e9b6 | ||
|
|
1ea96df00e | ||
|
|
75c5316dc3 | ||
|
|
4ee7623837 | ||
|
|
f65479ee15 | ||
|
|
a903e46be9 | ||
|
|
4497131206 | ||
|
|
4871abfb3a | ||
|
|
b0ed808745 | ||
|
|
169224560b | ||
|
|
106edc3a51 | ||
|
|
ede405c650 | ||
|
|
1dd1629884 | ||
|
|
0070c9877b | ||
|
|
eb208eed8b | ||
|
|
c37bf75853 | ||
|
|
7838008396 | ||
|
|
454c708fd6 | ||
|
|
3ffa78aa07 | ||
|
|
7dba9cbe54 | ||
|
|
716c4e9a21 | ||
|
|
f85a2d3ec1 | ||
|
|
d055dc68dc | ||
|
|
50a87ba933 | ||
|
|
6030cb560c | ||
|
|
c498230ebf | ||
|
|
4bbc5905e4 | ||
|
|
40f5db64d8 | ||
|
|
8d72a42be5 | ||
|
|
61c8cd75cd | ||
|
|
b46f6d0141 | ||
|
|
915d8c68bf | ||
|
|
21045142cd | ||
|
|
f171752109 | ||
|
|
82f00f961e | ||
|
|
b55b7fdc1c | ||
|
|
0b60060f65 | ||
|
|
b0f1266a8b | ||
|
|
863ff2e228 | ||
|
|
579aa5967f | ||
|
|
3bf5a6ebec | ||
|
|
ff222aa168 | ||
|
|
ea17534f77 | ||
|
|
b91d581928 | ||
|
|
4f6cb7f26c | ||
|
|
c1da3e7e95 | ||
|
|
040c568fdb | ||
|
|
7a78565c97 | ||
|
|
62e56eb27e | ||
|
|
8175120c4c | ||
|
|
6221fe5e08 | ||
|
|
4a8f882063 | ||
|
|
17db55a0bc | ||
|
|
a1d9268d00 | ||
|
|
14d227a1a3 | ||
|
|
3fd6ce1b6d | ||
|
|
7eb1b38cc7 | ||
|
|
2e9730ea1f | ||
|
|
a6cd4dfc0f | ||
|
|
0cf28c6392 | ||
|
|
7c2327ce16 | ||
|
|
099011a177 | ||
|
|
61d4a34f0f | ||
|
|
5b890dcd8a | ||
|
|
c2bd88377b | ||
|
|
efcc0d87b5 | ||
|
|
f3033956cf | ||
|
|
e26291943c | ||
|
|
3fc18feacf | ||
|
|
09a0b34df3 | ||
|
|
937be840c4 | ||
|
|
fef6df1321 | ||
|
|
daf804947c | ||
|
|
477d77cdc1 | ||
|
|
9bb012a439 | ||
|
|
d1650bb3c4 | ||
|
|
2ce22c18bf | ||
|
|
b48443c65b | ||
|
|
b7e7e5e603 | ||
|
|
fca5b7b544 | ||
|
|
3a40ee8713 | ||
|
|
9e4317a262 | ||
|
|
7fb6401613 | ||
|
|
d332032e53 | ||
|
|
560f21c0fe | ||
|
|
2f99628d94 | ||
|
|
ad3edf541b | ||
|
|
b07a1659b4 | ||
|
|
1a16d61511 | ||
|
|
366a0a6366 | ||
|
|
91b08e7380 | ||
|
|
9924faab84 | ||
|
|
9f356beec3 | ||
|
|
afe5f773e0 | ||
|
|
3e246f12de | ||
|
|
042451bad6 | ||
|
|
30106d5f82 | ||
|
|
e0d159c010 | ||
|
|
061c52c274 | ||
|
|
1bbb01bd42 | ||
|
|
3cabc3b967 | ||
|
|
7de4e55bad | ||
|
|
1f21d66140 | ||
|
|
3adee66899 | ||
|
|
67a6cd24cc | ||
|
|
a952318c77 | ||
|
|
336832ec40 | ||
|
|
543bd5acf7 | ||
|
|
6a0bf5858e | ||
|
|
11a75e7657 | ||
|
|
88635d8da8 | ||
|
|
934936eba8 | ||
|
|
53ec2c5306 | ||
|
|
cace1a54cd | ||
|
|
803c17de31 | ||
|
|
537eadef0c | ||
|
|
39f2b0ecb6 | ||
|
|
1e9e13aebb | ||
|
|
67835b04a8 | ||
|
|
3f93041ca2 | ||
|
|
0663b8adb0 | ||
|
|
1f48f5b5af | ||
|
|
0268600044 | ||
|
|
8fb38ba0f2 | ||
|
|
85c65e697d | ||
|
|
563997e060 | ||
|
|
2d5568ecec | ||
|
|
a64c4201bb | ||
|
|
51d5ed771c | ||
|
|
539132a527 | ||
|
|
9958600121 | ||
|
|
9ad01723a2 | ||
|
|
8448bacae7 | ||
|
|
c21e98a2a8 | ||
|
|
5bbc3872f3 | ||
|
|
8d894541e8 | ||
|
|
dc16a65b62 | ||
|
|
514c6f6e34 | ||
|
|
609e3896eb | ||
|
|
fd08dff1e7 | ||
|
|
6425ab8c57 | ||
|
|
e123e51c66 | ||
|
|
21fad12a21 | ||
|
|
924a77eb3f | ||
|
|
7aaf99a0c8 | ||
|
|
91accd5578 | ||
|
|
147f19f11e | ||
|
|
73ff6df73c | ||
|
|
0de780b7c9 | ||
|
|
f49f889536 | ||
|
|
b3f598aa1d | ||
|
|
cd1b5dcd3d | ||
|
|
79b7d269b0 | ||
|
|
40ac726930 | ||
|
|
ddcc14d99f | ||
|
|
3dd2de5f18 | ||
|
|
0a8f489234 | ||
|
|
1d64b2e22a | ||
|
|
251e89fe5a | ||
|
|
4fbbc2b1cf | ||
|
|
d5b7ac3542 | ||
|
|
fec395a4cf | ||
|
|
16c0dadc4a | ||
|
|
779092c0b7 | ||
|
|
9aa06fbc26 | ||
|
|
cfd2977c21 | ||
|
|
27fc0796a4 | ||
|
|
b1438bd460 | ||
|
|
267e0cb636 | ||
|
|
d471ef6763 | ||
|
|
cea5cfa4ab | ||
|
|
56356e8260 | ||
|
|
41e4086973 | ||
|
|
0f1f27d450 | ||
|
|
a558064f9d | ||
|
|
c421249767 | ||
|
|
8eff939039 | ||
|
|
e3444a3456 | ||
|
|
16a6f7b352 | ||
|
|
79c2633011 | ||
|
|
783aaa6553 | ||
|
|
60e0953616 | ||
|
|
f7c86f9fc9 | ||
|
|
fe684ab903 | ||
|
|
8592761493 | ||
|
|
dfe087df20 | ||
|
|
be3208c5a1 | ||
|
|
b56f39ce07 | ||
|
|
0178fddc38 | ||
|
|
31417a2aa1 | ||
|
|
f333a75221 | ||
|
|
bcf14a4c51 | ||
|
|
130c2863ab | ||
|
|
e59664426b | ||
|
|
2f0ef07944 | ||
|
|
9af19d01e5 | ||
|
|
e837d9fa30 | ||
|
|
93b79759c7 | ||
|
|
4becb2a822 | ||
|
|
134a621d0d | ||
|
|
089cca60ce | ||
|
|
20484ed7aa | ||
|
|
763a38812f | ||
|
|
7ccc32675b | ||
|
|
26feaf753a | ||
|
|
94c0003c19 | ||
|
|
c27f787c91 | ||
|
|
70e2138248 | ||
|
|
590ba3ce55 | ||
|
|
29671347a0 | ||
|
|
386e6e50da | ||
|
|
880ed21bcd | ||
|
|
bbc31ef077 | ||
|
|
9a13e977a0 | ||
|
|
a5602ff18c | ||
|
|
5901c5a535 | ||
|
|
cca1dd7e37 | ||
|
|
127e01cc96 | ||
|
|
1dcb38cb57 | ||
|
|
98b4970680 | ||
|
|
9706f1597b | ||
|
|
f8a5a77daa | ||
|
|
1efdceacfd | ||
|
|
b998c71337 | ||
|
|
6a6adb3480 | ||
|
|
a694aeed32 | ||
|
|
8182b795de | ||
|
|
608ab988f0 | ||
|
|
a261598e89 | ||
|
|
11a1918a2e | ||
|
|
67fb825937 | ||
|
|
4289f8b52d | ||
|
|
fb1ba51a2b | ||
|
|
cb47bc97c7 | ||
|
|
06e1bc9dd6 | ||
|
|
af39a1769c | ||
|
|
60ffa75653 | ||
|
|
7674433f88 | ||
|
|
7437a833ef | ||
|
|
1bad1a57b0 | ||
|
|
3437387885 | ||
|
|
23d098eee5 | ||
|
|
4636372ff4 | ||
|
|
4592786de8 | ||
|
|
f054f39c5d | ||
|
|
6e47de06bb | ||
|
|
c38751a38a | ||
|
|
2d218d79c6 | ||
|
|
5a41a35716 | ||
|
|
644554f1e9 | ||
|
|
3765a6ded8 | ||
|
|
92698e4bb5 | ||
|
|
17f395ee55 | ||
|
|
b11934054d | ||
|
|
40cbe92dbc | ||
|
|
b7f0988399 | ||
|
|
4c1020d2ba | ||
|
|
00f9cf0e2c | ||
|
|
1dd7e3bcdc | ||
|
|
3ac3122b31 | ||
|
|
6b5f8a41e7 | ||
|
|
8b39ba491a | ||
|
|
c74460fffa | ||
|
|
5000c4067b | ||
|
|
af33950bbe | ||
|
|
075331b26c | ||
|
|
f31b74f7fd |
+1
-4
@@ -1,9 +1,6 @@
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
|
||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||
[target.'cfg(windows)']
|
||||
rustflags = ["--cfg", "tokio_unstable", "-C", "link-args=/STACK:16777220"]
|
||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld"
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# Adding a New API Module
|
||||
|
||||
How to add a new API endpoint module to `packages/api-client`.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Define types in the module's `types.ts`
|
||||
|
||||
Types must match 1:1 with the backend API response. Do not reshape, rename, or omit fields.
|
||||
|
||||
Add to an existing namespace or create a new one:
|
||||
|
||||
```ts
|
||||
// modules/labrinth/types.ts (existing namespace)
|
||||
export namespace Labrinth {
|
||||
export namespace MyDomain {
|
||||
export namespace v3 {
|
||||
export type Thing = {
|
||||
id: string
|
||||
name: string
|
||||
created: string
|
||||
// ... matches API response exactly
|
||||
}
|
||||
|
||||
export type CreateThingRequest = {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For a new API service, create `modules/<service>/types.ts` with a new top-level namespace and re-export it from `modules/types.ts`.
|
||||
|
||||
### 2. Create the module class
|
||||
|
||||
Create `modules/<api>/<domain>/v<N>.ts`:
|
||||
|
||||
```ts
|
||||
// modules/labrinth/things/v3.ts
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthThingsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_things_v3'
|
||||
}
|
||||
|
||||
public async get(id: string): Promise<Labrinth.MyDomain.v3.Thing> {
|
||||
return this.client.request<Labrinth.MyDomain.v3.Thing>(`/thing/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
public async create(data: Labrinth.MyDomain.v3.CreateThingRequest): Promise<Labrinth.MyDomain.v3.Thing> {
|
||||
return this.client.request<Labrinth.MyDomain.v3.Thing>(`/thing`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.client.request(`/thing/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Request options
|
||||
|
||||
| Field | Values | Purpose |
|
||||
|-------|--------|---------|
|
||||
| `api` | `'labrinth'`, `'archon'`, or a full URL | Which base URL to use |
|
||||
| `version` | `2`, `3`, `'internal'`, `'modrinth/v0'`, etc. | URL version segment |
|
||||
| `method` | `'GET'`, `'POST'`, `'PUT'`, `'PATCH'`, `'DELETE'` | HTTP method |
|
||||
| `body` | object | JSON request body |
|
||||
| `params` | `Record<string, string>` | Query parameters |
|
||||
| `skipAuth` | `boolean` | Skip auth feature for this request |
|
||||
| `useNodeAuth` | `boolean` | Use node-level auth (kyros) |
|
||||
| `timeout` | `number` | Request timeout in ms |
|
||||
| `retry` | `boolean \| number` | Override retry behavior |
|
||||
|
||||
#### For uploads
|
||||
|
||||
Return an `UploadHandle` instead of a `Promise`:
|
||||
|
||||
```ts
|
||||
public uploadThing(id: string, file: File): UploadHandle<void> {
|
||||
return this.client.upload<void>(`/thing/${id}/file`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
file,
|
||||
})
|
||||
}
|
||||
|
||||
// Or with FormData for multipart:
|
||||
public createWithFiles(data: CreateRequest, files: File[]): UploadHandle<Thing> {
|
||||
const formData = new FormData()
|
||||
formData.append('data', JSON.stringify(data))
|
||||
files.forEach((f, i) => formData.append(`file-${i}`, f, f.name))
|
||||
|
||||
return this.client.upload<Thing>(`/thing`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
formData,
|
||||
timeout: 60 * 5 * 1000, // longer timeout for uploads
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Register in the MODULE_REGISTRY
|
||||
|
||||
Add to `modules/index.ts`:
|
||||
|
||||
```ts
|
||||
import { LabrinthThingsV3Module } from './labrinth/things/v3'
|
||||
|
||||
export const MODULE_REGISTRY = {
|
||||
// ... existing modules
|
||||
labrinth_things_v3: LabrinthThingsV3Module,
|
||||
} as const
|
||||
```
|
||||
|
||||
The naming convention is `<api>_<domain>_<version>`. This flat key gets transformed into nested access: `client.labrinth.things_v3`.
|
||||
|
||||
### 4. Export types
|
||||
|
||||
If you added to an existing namespace, types are already re-exported. If you created a new `types.ts`, add it to `modules/types.ts`:
|
||||
|
||||
```ts
|
||||
export * from './<service>/types'
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Convention | Example |
|
||||
|-----------|---------|
|
||||
| Module class | `LabrinthThingsV3Module` — `{Api}{Domain}V{N}Module` |
|
||||
| Module ID | `labrinth_things_v3` — `{api}_{domain}_v{n}` |
|
||||
| Type namespace | `Labrinth.MyDomain.v3.Thing` |
|
||||
| File path | `modules/labrinth/things/v3.ts` |
|
||||
|
||||
## Key Files
|
||||
|
||||
- `src/core/abstract-module.ts` — base class all modules extend
|
||||
- `src/core/abstract-client.ts` — `request()` and `upload()` methods
|
||||
- `src/modules/index.ts` — `MODULE_REGISTRY` and `buildModuleStructure()`
|
||||
- `src/modules/<api>/types.ts` — type definitions per API
|
||||
- `src/types/upload.ts` — `UploadHandle`, `UploadProgress`, `UploadRequestOptions`
|
||||
@@ -0,0 +1,144 @@
|
||||
# Cross-Platform Page System
|
||||
|
||||
When a page needs to exist in both the Modrinth App (`apps/app-frontend`) and the Modrinth Website (`apps/frontend`), use the cross-platform page system.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Pages live as Vue SFCs in `packages/ui`** — either in `src/pages/` or `src/layout/` (if `src/pages/` doesn't exist, it's been renamed to `src/layout/`).
|
||||
2. **Platform-dependent data flows via DI** — the app uses Tauri `invoke` commands, the website uses `api-client` or the legacy `useBaseFetch` composable. The shared page never knows which. See the `dependency-injection` skill for full DI docs.
|
||||
3. **Non-platform-dependent data flows via props** — if data doesn't change based on _how_ it's fetched, just pass it as a prop.
|
||||
|
||||
## Example: Content Page
|
||||
|
||||
`ContentPageLayout` demonstrates the full pattern.
|
||||
|
||||
### 1. Define a DI contract in `packages/ui/src/providers/`
|
||||
|
||||
The provider interface abstracts all platform-specific operations:
|
||||
|
||||
```ts
|
||||
// packages/ui/src/providers/content-manager.ts
|
||||
export interface ContentManagerContext {
|
||||
items: Ref<ContentItem[]>
|
||||
loading: Ref<boolean>
|
||||
error: Ref<Error | null>
|
||||
contentTypeLabel: Ref<string>
|
||||
|
||||
// These are the platform-abstracted operations:
|
||||
// App uses invoke(), website uses api-client
|
||||
toggleEnabled: (item: ContentItem) => Promise<void>
|
||||
deleteItem: (item: ContentItem) => Promise<void>
|
||||
refresh: () => Promise<void>
|
||||
browse: () => void
|
||||
uploadFiles: () => void
|
||||
|
||||
// Optional capabilities — not every platform supports everything
|
||||
hasUpdateSupport: boolean
|
||||
updateItem?: (item: ContentItem) => Promise<void>
|
||||
bulkUpdateItem?: (items: ContentItem[]) => Promise<void>
|
||||
|
||||
mapToTableItem: (item: ContentItem) => ContentCardTableItem
|
||||
}
|
||||
|
||||
export const [injectContentManager, provideContentManager] =
|
||||
createContext<ContentManagerContext>('ContentManager')
|
||||
```
|
||||
|
||||
### 2. Build the shared page in `packages/ui`
|
||||
|
||||
The page component injects the context and handles all UI logic (search, filtering, selection, bulk operations, empty states, modals) without knowing the platform:
|
||||
|
||||
```vue
|
||||
<!-- packages/ui/src/components/instances/ContentPageLayout.vue -->
|
||||
<script setup lang="ts">
|
||||
import { injectContentManager } from '../../providers/content-manager'
|
||||
|
||||
const { items, loading, toggleEnabled, deleteItem, refresh, mapToTableItem } =
|
||||
injectContentManager()
|
||||
|
||||
// All UI logic lives here — search, filters, sort, bulk ops, etc.
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentCardTable :items="filteredItems" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. Each platform provides its implementation
|
||||
|
||||
**Website (Nuxt)** — uses `api-client` or `useBaseFetch`:
|
||||
|
||||
```vue
|
||||
<!-- apps/frontend/src/pages/hosting/manage/[id]/content.vue -->
|
||||
<script setup lang="ts">
|
||||
import { provideContentManager, ContentPageLayout } from '@modrinth/ui'
|
||||
const { labrinth } = injectModrinthClient()
|
||||
|
||||
const { data: items } = useQuery({
|
||||
queryKey: ['content', serverId],
|
||||
queryFn: () => labrinth.servers_v0.getAddons(serverId),
|
||||
})
|
||||
|
||||
provideContentManager({
|
||||
items: computed(() => items.value?.map(addonToContentItem) ?? []),
|
||||
deleteItem: async (item) => {
|
||||
await labrinth.servers_v0.deleteAddon(serverId, item.id)
|
||||
},
|
||||
// ... rest of the contract
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentPageLayout />
|
||||
</template>
|
||||
```
|
||||
|
||||
**App (Tauri)** — uses `invoke`:
|
||||
|
||||
```vue
|
||||
<!-- apps/app-frontend/src/pages/instance/Content.vue -->
|
||||
<script setup lang="ts">
|
||||
import { provideContentManager, ContentPageLayout } from '@modrinth/ui'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
const items = ref<ContentItem[]>([])
|
||||
await invoke('get_instance_content', { instanceId }).then(/* map to ContentItem[] */)
|
||||
|
||||
provideContentManager({
|
||||
items,
|
||||
deleteItem: async (item) => {
|
||||
await invoke('delete_content', { instanceId, path: item.file_path })
|
||||
},
|
||||
// ... rest of the contract
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentPageLayout />
|
||||
</template>
|
||||
```
|
||||
|
||||
## When to Use Props vs DI
|
||||
|
||||
| Use | When |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| **DI** | The data depends on _how_ it's fetched (different per platform) — API calls, file operations, navigation |
|
||||
| **Props** | The data is the same regardless of platform — configuration flags, display options |
|
||||
|
||||
## Composables for Shared Logic
|
||||
|
||||
Extract reusable stateful logic into composables in `packages/ui/src/composables/`. The shared page orchestrates them internally:
|
||||
|
||||
- Search (Fuse.js fuzzy search over items)
|
||||
- Filtering (dynamic filter pills)
|
||||
- Selection (multi-select with bulk operations)
|
||||
- Bulk operations (sequential execution with progress tracking)
|
||||
|
||||
## Key Files
|
||||
|
||||
- `packages/ui/src/pages/` (or `src/layout/`) — shared page components
|
||||
- `packages/ui/src/providers/` — DI contracts
|
||||
- `packages/ui/src/composables/` — shared stateful logic
|
||||
- `apps/frontend/src/app.vue` — website root provider setup
|
||||
- `apps/app-frontend/src/App.vue` — app root provider setup
|
||||
- `apps/app-frontend/src/routes.js` — app route definitions
|
||||
@@ -0,0 +1,174 @@
|
||||
# Dependency Injection
|
||||
|
||||
Modrinth uses a lightweight DI layer built on Vue's `provide`/`inject` for sharing platform-specific capabilities and page-level state across shared UI components.
|
||||
|
||||
## The `createContext` Factory
|
||||
|
||||
All providers are defined using `createContext` from `packages/ui/src/providers/index.ts` (adapted from Reka UI). It produces a typed `[inject, provide]` tuple:
|
||||
|
||||
```ts
|
||||
import { createContext } from '@modrinth/ui'
|
||||
|
||||
interface MyContext {
|
||||
someValue: Ref<string>
|
||||
doSomething: () => void
|
||||
}
|
||||
|
||||
export const [injectMyContext, provideMyContext] = createContext<MyContext>('MyComponent')
|
||||
```
|
||||
|
||||
- **`provideMyContext(value)`** — call in a parent component's `setup()`.
|
||||
- **`injectMyContext()`** — call in any descendant's `setup()`. Throws if never provided.
|
||||
- **`injectMyContext(null)`** — returns `null` instead of throwing (for optional contexts).
|
||||
|
||||
## When to Use DI
|
||||
|
||||
Use DI when:
|
||||
- **The same interface needs different implementations** depending on the platform (web vs desktop app).
|
||||
- **Deeply nested components** need access to shared page-level state without prop drilling through 3+ levels.
|
||||
|
||||
### Platform Abstraction (Primary Use Case)
|
||||
|
||||
`packages/ui` components need capabilities that each frontend fulfils differently:
|
||||
|
||||
| Provider | App Frontend | Website Frontend |
|
||||
|----------|-------------|-----------------|
|
||||
| API client | Tauri IPC client | REST fetch client |
|
||||
| Notifications | `ref()` state + app window mgmt | `useState()` for SSR hydration |
|
||||
| File picker | Native Tauri dialogs | Browser file inputs |
|
||||
| Tags | Tauri commands | Nuxt server state |
|
||||
| Page context | `sidebar: true`, ad window hooks | `sidebar: false`, no ads |
|
||||
|
||||
### Page-Level Context
|
||||
|
||||
Sharing data between a page and deeply nested children — e.g. project page data consumed by sidebar, header, and version components.
|
||||
|
||||
## Creating a New Provider
|
||||
|
||||
### 1. Define the interface in `packages/ui/src/providers/`
|
||||
|
||||
```ts
|
||||
// packages/ui/src/providers/my-feature.ts
|
||||
import type { Ref } from 'vue'
|
||||
import { createContext } from '.'
|
||||
|
||||
export interface MyFeatureContext {
|
||||
items: Ref<Item[]>
|
||||
addItem: (item: Item) => Promise<void>
|
||||
removeItem: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const [injectMyFeature, provideMyFeature] = createContext<MyFeatureContext>('MyFeature')
|
||||
```
|
||||
|
||||
Re-export from the barrel file (`packages/ui/src/providers/index.ts`).
|
||||
|
||||
### 2. For complex platform-specific logic, use an abstract class
|
||||
|
||||
```ts
|
||||
export abstract class AbstractMyFeatureManager {
|
||||
abstract items: Ref<Item[]>
|
||||
abstract addItem(item: Item): Promise<void>
|
||||
|
||||
// Shared logic lives on the base class
|
||||
handleError(err: unknown) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const [injectMyFeature, provideMyFeature] =
|
||||
createContext<AbstractMyFeatureManager>('MyFeature')
|
||||
```
|
||||
|
||||
See `AbstractWebNotificationManager` in `packages/ui/src/providers/web-notifications.ts` for a real example.
|
||||
|
||||
## Wiring Up Providers
|
||||
|
||||
### App Frontend (Tauri)
|
||||
|
||||
Create a setup function in `apps/app-frontend/src/providers/setup/`:
|
||||
|
||||
```ts
|
||||
// apps/app-frontend/src/providers/setup/my-feature.ts
|
||||
import { ref } from 'vue'
|
||||
import { provideMyFeature } from '@modrinth/ui'
|
||||
|
||||
export function setupMyFeatureProvider() {
|
||||
const items = ref<Item[]>([])
|
||||
|
||||
provideMyFeature({
|
||||
items,
|
||||
addItem: async (item) => {
|
||||
await invoke('add_item', { item })
|
||||
items.value.push(item)
|
||||
},
|
||||
removeItem: async (id) => {
|
||||
await invoke('remove_item', { id })
|
||||
items.value = items.value.filter(i => i.id !== id)
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Register it in `apps/app-frontend/src/providers/setup.ts`, which is called from `App.vue`'s `setup()`.
|
||||
|
||||
### Website Frontend (Nuxt)
|
||||
|
||||
Provide directly in `apps/frontend/src/app.vue`, using Nuxt's `useState()` where SSR hydration is needed:
|
||||
|
||||
```ts
|
||||
provideMyFeature({
|
||||
items: useState<Item[]>('my-feature-items', () => []),
|
||||
addItem: async (item) => {
|
||||
await $fetch('/api/items', { method: 'POST', body: item })
|
||||
},
|
||||
removeItem: async (id) => {
|
||||
await $fetch(`/api/items/${id}`, { method: 'DELETE' })
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Consuming Providers
|
||||
|
||||
In any component across `packages/ui`, `apps/frontend`, or `apps/app-frontend`:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { injectMyFeature } from '@modrinth/ui'
|
||||
|
||||
const { items, addItem } = injectMyFeature()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
|
||||
<button @click="addItem({ id: '1', name: 'New' })">Add</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
## When NOT to Use DI
|
||||
|
||||
Default to props and emits. DI adds indirection — only use it with a concrete reason.
|
||||
|
||||
- **Parent to direct child** — use props.
|
||||
- **Data only exists in one frontend** — keep context local to that app, not in `packages/ui`.
|
||||
- **Shallow prop drilling (1–2 levels)** — passing through one intermediate is fine.
|
||||
- **Component-local state** — use `ref()` / `reactive()` locally.
|
||||
|
||||
## Existing Providers
|
||||
|
||||
| Provider | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| `provideModrinthClient` | `providers/api-client.ts` | API client instance |
|
||||
| `provideNotificationManager` | `providers/web-notifications.ts` | Notification management |
|
||||
| `providePageContext` | `providers/page-context.ts` | Page config (sidebar, ads) |
|
||||
| `provideProjectPageContext` | `providers/project-page.ts` | Project page state + mutations |
|
||||
| `provideServerContext` | `providers/server-context.ts` | Server hosting state |
|
||||
| `provideUserPageContext` | `providers/user-page.ts` | User page state |
|
||||
|
||||
## Key Files
|
||||
|
||||
- `packages/ui/src/providers/index.ts` — `createContext` factory + barrel exports
|
||||
- `packages/ui/src/providers/*.ts` — Provider definitions
|
||||
- `apps/frontend/src/app.vue` — Nuxt root provider setup
|
||||
- `apps/app-frontend/src/App.vue` — Tauri root provider setup
|
||||
- `apps/app-frontend/src/providers/setup/` — App provider setup functions
|
||||
@@ -0,0 +1,45 @@
|
||||
# Figma MCP Usage
|
||||
|
||||
When the Figma MCP server is connected, use it to translate Figma designs into production-ready Vue components for this monorepo.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Get the design context
|
||||
|
||||
Use `get_design_context` with the node ID from a Figma URL. If the URL is `https://figma.com/design/:fileKey/:fileName?node-id=1-2`, the node ID is `1:2`.
|
||||
|
||||
```
|
||||
get_design_context(nodeId: "1:2", clientLanguages: "typescript,html,css", clientFrameworks: "vue")
|
||||
```
|
||||
|
||||
This returns reference code, a screenshot, and metadata. Always start here.
|
||||
|
||||
### 2. Get a screenshot for visual reference
|
||||
|
||||
Use `get_screenshot` if you need to see the design without full code context:
|
||||
|
||||
```
|
||||
get_screenshot(nodeId: "1:2")
|
||||
```
|
||||
|
||||
### 3. Get variable definitions
|
||||
|
||||
Use `get_variable_defs` to see what design tokens are applied to a node:
|
||||
|
||||
```
|
||||
get_variable_defs(nodeId: "1:2")
|
||||
```
|
||||
|
||||
### 4. Get metadata for structure overview
|
||||
|
||||
Use `get_metadata` to get an XML overview of node IDs, layer types, names, positions and sizes — useful for understanding the structure of a complex frame before diving into individual nodes.
|
||||
|
||||
## Adapting Figma Output
|
||||
|
||||
The Figma MCP returns generic reference code. Adapt it to match the Modrinth codebase:
|
||||
|
||||
1. **Read `packages/ui/CLAUDE.md`** for color usage rules, surface token mapping, and component patterns.
|
||||
2. **Map Figma color variables to `surface-*` tokens** — never use Figma's aliased names like `bg/default` or `bg/raised` directly. The CLAUDE.md has the full mapping table.
|
||||
3. **Check `packages/assets/styles/variables.scss`** for tokens not exposed in Figma (brand highlights, semantic backgrounds, shadows).
|
||||
4. **Check for existing components** in `packages/ui/src/components/` before building from scratch.
|
||||
5. **Match spacing exactly** — do not approximate values from the design.
|
||||
@@ -0,0 +1,105 @@
|
||||
# i18n String Conversion
|
||||
|
||||
Convert hard-coded natural-language strings in Vue SFCs into the localization system using utilities from `@modrinth/ui`.
|
||||
|
||||
## Rules
|
||||
|
||||
### 1. Identify translatable strings
|
||||
|
||||
- Scan `<template>` for all user-visible strings: inner text, alt attributes, placeholders, button labels, etc.
|
||||
- Check `<script>` too: dropdown option labels, notification messages, etc.
|
||||
- Do NOT extract dynamic expressions (`{{ user.name }}`) or HTML tags — only static human-readable text.
|
||||
|
||||
### 2. Create message definitions
|
||||
|
||||
Import `defineMessage` or `defineMessages` from `@modrinth/ui` in `<script setup>`. Define messages with a unique `id` (descriptive prefix based on component path) and `defaultMessage` equal to the original English string:
|
||||
|
||||
```ts
|
||||
const messages = defineMessages({
|
||||
welcomeTitle: { id: 'auth.welcome.title', defaultMessage: 'Welcome' },
|
||||
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: "You're now part of the community…" },
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Handle variables and ICU formats
|
||||
|
||||
- Dynamic parts become ICU placeholders: `"Hello, ${user.name}!"` → `defaultMessage: 'Hello, {name}!'`
|
||||
- Numbers/dates/times use ICU options: `{price, number, ::currency/USD}`
|
||||
- Plurals/selects use ICU: `'{count, plural, one {# message} other {# messages}}'`
|
||||
|
||||
### 4. Rich-text messages (links/markup)
|
||||
|
||||
Wrap link/markup ranges with tags in `defaultMessage`:
|
||||
|
||||
```
|
||||
"By creating an account, you agree to our <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."
|
||||
```
|
||||
|
||||
Render with `<IntlFormatted>` from `@modrinth/ui` using named slots:
|
||||
|
||||
```vue
|
||||
<IntlFormatted :message-id="messages.tosLabel">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/terms">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #privacy-link="{ children }">
|
||||
<NuxtLink to="/privacy">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
```
|
||||
|
||||
For simple emphasis: `'Welcome to <strong>Modrinth</strong>!'` with a slot:
|
||||
|
||||
```vue
|
||||
<template #strong="{ children }">
|
||||
<strong><component :is="() => children" /></strong>
|
||||
</template>
|
||||
```
|
||||
|
||||
For complex child handling, use `normalizeChildren` from `@modrinth/ui`:
|
||||
|
||||
```vue
|
||||
<template #bold="{ children }">
|
||||
<strong><component :is="() => normalizeChildren(children)" /></strong>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 5. Formatting in templates
|
||||
|
||||
Use `useVIntl()` from `@modrinth/ui`; prefer `formatMessage` for simple strings:
|
||||
|
||||
```ts
|
||||
const { formatMessage } = useVIntl()
|
||||
```
|
||||
|
||||
```vue
|
||||
<button>{{ formatMessage(messages.welcomeTitle) }}</button>
|
||||
{{ formatMessage(messages.greeting, { name: user.name }) }}
|
||||
```
|
||||
|
||||
### 6. Naming conventions
|
||||
|
||||
Make `id`s descriptive and stable (e.g., `error.generic.default.title`). Group related messages with `defineMessages`.
|
||||
|
||||
### 7. Avoid Vue/ICU delimiter collisions
|
||||
|
||||
If an ICU placeholder ends right before `}}` in a Vue template, insert a space: `} }` to avoid parsing issues.
|
||||
|
||||
### 8. Imports
|
||||
|
||||
Ensure these are imported from `@modrinth/ui` as needed: `defineMessage`/`defineMessages`, `useVIntl`, `IntlFormatted`, `normalizeChildren`.
|
||||
|
||||
### 9. Preserve functionality
|
||||
|
||||
Do not change logic, layout, reactivity, or bindings — only refactor strings into i18n.
|
||||
|
||||
## Reference Examples
|
||||
|
||||
- Variables/plurals: `apps/frontend/src/pages/frog.vue`
|
||||
- Rich-text link tags: `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue`
|
||||
|
||||
When finished, there should be no hard-coded English strings left in the template — everything comes from `formatMessage` or `<IntlFormatted>`.
|
||||
@@ -0,0 +1,215 @@
|
||||
# Multistage Modals
|
||||
|
||||
The `MultiStageModal` component (`packages/ui/src/components/base/MultiStageModal.vue`) provides a wizard-like modal with progress tracking, conditional stages, and per-stage button configuration.
|
||||
|
||||
## Architecture
|
||||
|
||||
A multistage modal has three parts:
|
||||
|
||||
1. **Context** — A DI provider that holds all state, business logic, and stage configs
|
||||
2. **Stage configs** — Data objects describing each stage (title, component, buttons, skip conditions)
|
||||
3. **Stage components** — Vue components rendered inside the modal, consuming the context
|
||||
|
||||
## Building a Multistage Modal
|
||||
|
||||
### 1. Define the context
|
||||
|
||||
Create a DI provider with all the state your wizard needs. Include the modal ref and stage configs.
|
||||
|
||||
```ts
|
||||
// providers/my-feature/my-modal.ts
|
||||
import type { ShallowRef } from 'vue'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
import type { MultiStageModal, StageConfigInput } from '@modrinth/ui'
|
||||
import { createContext } from '@modrinth/ui'
|
||||
|
||||
export interface MyModalContext {
|
||||
// State
|
||||
formData: Ref<MyFormData>
|
||||
isSubmitting: Ref<boolean>
|
||||
|
||||
// Modal control
|
||||
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>
|
||||
stageConfigs: StageConfigInput<MyModalContext>[]
|
||||
|
||||
// Business logic
|
||||
handleSubmit: () => Promise<void>
|
||||
}
|
||||
|
||||
export const [injectMyModalContext, provideMyModalContext] =
|
||||
createContext<MyModalContext>('MyModal')
|
||||
|
||||
export function createMyModalContext(
|
||||
modal: ShallowRef<ComponentExposed<typeof MultiStageModal> | null>,
|
||||
): MyModalContext {
|
||||
const formData = ref<MyFormData>({ ... })
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
await saveData(formData.value)
|
||||
modal.value?.hide()
|
||||
} finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { formData, isSubmitting, modal, stageConfigs, handleSubmit }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Define stage configs
|
||||
|
||||
Each stage is a `StageConfigInput<T>` where `T` is your context type. Most fields accept either a static value or a function receiving the context (`MaybeCtxFn<T, R>`).
|
||||
|
||||
```ts
|
||||
// providers/my-feature/stages/details-stage.ts
|
||||
import { markRaw } from 'vue'
|
||||
import type { StageConfigInput } from '@modrinth/ui'
|
||||
import type { MyModalContext } from '../my-modal'
|
||||
import DetailsStage from './DetailsStage.vue'
|
||||
import { RightArrowIcon, SaveIcon } from '@modrinth/assets'
|
||||
|
||||
export const detailsStageConfig: StageConfigInput<MyModalContext> = {
|
||||
id: 'details',
|
||||
stageContent: markRaw(DetailsStage),
|
||||
title: 'Details',
|
||||
|
||||
// Conditional behavior based on context
|
||||
skip: (ctx) => ctx.shouldSkipDetails.value,
|
||||
cannotNavigateForward: (ctx) => !ctx.formData.value.name,
|
||||
disableClose: (ctx) => ctx.isSubmitting.value,
|
||||
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Cancel',
|
||||
onClick: () => ctx.modal.value?.hide(),
|
||||
}),
|
||||
|
||||
rightButtonConfig: (ctx) => ({
|
||||
label: 'Next',
|
||||
icon: RightArrowIcon,
|
||||
iconPosition: 'after',
|
||||
disabled: !ctx.formData.value.name,
|
||||
onClick: () => ctx.modal.value?.nextStage(),
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
**Stage config fields:**
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `id` | `string` | Unique stage identifier (used with `setStage()`) |
|
||||
| `stageContent` | `Component` | Vue component to render (wrap with `markRaw()`) |
|
||||
| `title` | `MaybeCtxFn<T, string>` | Stage title in breadcrumbs |
|
||||
| `skip` | `MaybeCtxFn<T, boolean>` | Skip this stage conditionally |
|
||||
| `nonProgressStage` | `MaybeCtxFn<T, boolean>` | Exclude from progress bar (for edit sub-flows) |
|
||||
| `hideStageInBreadcrumb` | `MaybeCtxFn<T, boolean>` | Hide from breadcrumb nav |
|
||||
| `cannotNavigateForward` | `MaybeCtxFn<T, boolean>` | Block forward navigation (validation) |
|
||||
| `disableClose` | `MaybeCtxFn<T, boolean>` | Disable closing the modal |
|
||||
| `leftButtonConfig` | `MaybeCtxFn<T, StageButtonConfig \| null>` | Left action button |
|
||||
| `rightButtonConfig` | `MaybeCtxFn<T, StageButtonConfig \| null>` | Right action button |
|
||||
| `maxWidth` | `MaybeCtxFn<T, string>` | Per-stage max width (default `560px`) |
|
||||
|
||||
**Button config fields:**
|
||||
|
||||
| Field | Purpose |
|
||||
|-------|---------|
|
||||
| `label` | Button text |
|
||||
| `icon` | Icon component |
|
||||
| `iconPosition` | `'before'` or `'after'` |
|
||||
| `color` | ButtonStyled color prop |
|
||||
| `disabled` | Disable the button |
|
||||
| `onClick` | Click handler |
|
||||
|
||||
### 3. Create stage components
|
||||
|
||||
Stage components inject the context and render their UI:
|
||||
|
||||
```vue
|
||||
<!-- providers/my-feature/stages/DetailsStage.vue -->
|
||||
<script setup lang="ts">
|
||||
import { injectMyModalContext } from '../my-modal'
|
||||
|
||||
const { formData } = injectMyModalContext()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<StyledInput v-model="formData.name" label="Name" />
|
||||
<StyledInput v-model="formData.description" label="Description" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 4. Create the wrapper component
|
||||
|
||||
The wrapper provides context and renders `MultiStageModal`:
|
||||
|
||||
```vue
|
||||
<!-- components/MyModalWrapper.vue -->
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue'
|
||||
import { MultiStageModal } from '@modrinth/ui'
|
||||
import { createMyModalContext, provideMyModalContext } from '../providers/my-feature/my-modal'
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MultiStageModal> | null>(null)
|
||||
const ctx = createMyModalContext(modal)
|
||||
provideMyModalContext(ctx)
|
||||
|
||||
defineExpose({ show: () => modal.value?.show() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MultiStageModal ref="modal" :stages="ctx.stageConfigs" :context="ctx" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Modal API
|
||||
|
||||
`MultiStageModal` exposes via ref:
|
||||
|
||||
| Method/Property | Description |
|
||||
|----------------|-------------|
|
||||
| `show()` | Open the modal |
|
||||
| `hide()` | Close the modal |
|
||||
| `setStage(indexOrId)` | Jump to stage by index or string id |
|
||||
| `nextStage()` | Advance to next non-skipped stage |
|
||||
| `prevStage()` | Go back to previous stage |
|
||||
| `currentStageIndex` | Ref to current stage index |
|
||||
|
||||
## Non-Progress Stages (Edit Sub-Flows)
|
||||
|
||||
For stages that shouldn't appear in the progress bar (e.g. editing a specific field from a summary page):
|
||||
|
||||
```ts
|
||||
export const editLoadersStageConfig: StageConfigInput<MyContext> = {
|
||||
id: 'edit-loaders',
|
||||
nonProgressStage: true,
|
||||
stageContent: markRaw(EditLoadersStage),
|
||||
title: 'Edit loaders',
|
||||
leftButtonConfig: (ctx) => ({
|
||||
label: 'Back',
|
||||
onClick: () => ctx.modal.value?.setStage('summary'),
|
||||
}),
|
||||
rightButtonConfig: (ctx) => ({
|
||||
...ctx.saveButtonConfig(),
|
||||
label: 'Save',
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Navigate to it with `modal.value?.setStage('edit-loaders')` — it won't affect the progress indicator.
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
The version creation/edit modal is the most complete example:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `apps/frontend/src/providers/version/manage-version-modal.ts` | Context creation + business logic |
|
||||
| `apps/frontend/src/providers/version/stages/index.ts` | Stage config barrel export |
|
||||
| `apps/frontend/src/providers/version/stages/*-stage.ts` | Individual stage configs |
|
||||
|
||||
The context includes computed properties for conditional UI, watchers for auto-fetching dependencies, loading states for granular button disabling, and both "create" and "edit" flows sharing the same stages with different button configs.
|
||||
@@ -0,0 +1,154 @@
|
||||
# TanStack Query
|
||||
|
||||
TanStack Query (`@tanstack/vue-query` v5) is used for server state management — caching, background refetching, and cache invalidation. Use it instead of manual `ref()` + `await` patterns for any data that comes from an API.
|
||||
|
||||
A TanStack MCP server is available — use `tanstack_doc` and `tanstack_search_docs` tools to look up API details when needed.
|
||||
|
||||
## Setup
|
||||
|
||||
TanStack Query is configured in `apps/frontend/src/plugins/tanstack.ts` as a Nuxt plugin with SSR hydration support. Default stale time is 5 seconds. The `QueryClient` is available via `useQueryClient()` or `useAppQueryClient()` (which also works in middleware).
|
||||
|
||||
## Queries
|
||||
|
||||
Use `useQuery` with the api-client for data fetching:
|
||||
|
||||
```ts
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const { data, isPending, isError, error } = useQuery({
|
||||
queryKey: ['project', 'v3', projectId],
|
||||
queryFn: () => client.labrinth.projects_v3.get(projectId),
|
||||
staleTime: 1000 * 60 * 5,
|
||||
})
|
||||
```
|
||||
|
||||
In templates:
|
||||
|
||||
```vue
|
||||
<span v-if="isPending">Loading...</span>
|
||||
<span v-else-if="isError">Error: {{ error.message }}</span>
|
||||
<div v-else>{{ data.title }}</div>
|
||||
```
|
||||
|
||||
### Query Option Factories
|
||||
|
||||
For queries used across multiple components, define reusable query option factories in `packages/ui/src/queries/`:
|
||||
|
||||
```ts
|
||||
// composables/queries/project.ts
|
||||
export const STALE_TIME = 1000 * 60 * 5
|
||||
export const STALE_TIME_LONG = 1000 * 60 * 10
|
||||
|
||||
export const projectQueryOptions = {
|
||||
v3: (projectId: string, client: AbstractModrinthClient) => ({
|
||||
queryKey: ['project', 'v3', projectId] as const,
|
||||
queryFn: () => client.labrinth.projects_v3.get(projectId),
|
||||
staleTime: STALE_TIME,
|
||||
}),
|
||||
|
||||
members: (projectId: string, client: AbstractModrinthClient) => ({
|
||||
queryKey: ['project', projectId, 'members'] as const,
|
||||
queryFn: () => client.labrinth.projects_v3.getMembers(projectId),
|
||||
staleTime: STALE_TIME,
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Then use them:
|
||||
|
||||
```ts
|
||||
const { data } = useQuery(projectQueryOptions.v3(projectId, client))
|
||||
```
|
||||
|
||||
### Conditional Queries
|
||||
|
||||
Use `enabled` as a computed for queries that depend on other data:
|
||||
|
||||
```ts
|
||||
const { data: members } = useQuery({
|
||||
queryKey: ['project', projectId, 'members'],
|
||||
queryFn: () => client.labrinth.projects_v3.getMembers(projectId),
|
||||
enabled: computed(() => !!projectId.value),
|
||||
})
|
||||
```
|
||||
|
||||
## Mutations
|
||||
|
||||
Use `useMutation` for create/update/delete operations. Invalidate related queries on success:
|
||||
|
||||
```ts
|
||||
const queryClient = useQueryClient()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (name: string) => client.archon.backups_v0.create(serverId, { name }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['backups', 'list', serverId] }),
|
||||
})
|
||||
```
|
||||
|
||||
Use `createMutation.isPending.value` to disable buttons during submission.
|
||||
|
||||
### Optimistic Updates
|
||||
|
||||
For mutations where responsiveness matters, use optimistic updates with rollback:
|
||||
|
||||
```ts
|
||||
const patchMutation = useMutation({
|
||||
mutationFn: async ({ projectId, data }) => {
|
||||
await client.labrinth.projects_v3.patch(projectId, data)
|
||||
return data
|
||||
},
|
||||
|
||||
onMutate: async ({ projectId, data }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['project', 'v3', projectId] })
|
||||
const previous = queryClient.getQueryData(['project', 'v3', projectId])
|
||||
|
||||
queryClient.setQueryData(['project', 'v3', projectId], (old) => {
|
||||
if (!old) return old
|
||||
return { ...old, ...data }
|
||||
})
|
||||
|
||||
return { previous }
|
||||
},
|
||||
|
||||
onError: (_err, _variables, context) => {
|
||||
if (context?.previous) {
|
||||
queryClient.setQueryData(['project', 'v3', projectId], context.previous)
|
||||
}
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['project', 'v3', projectId] })
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Query Keys
|
||||
|
||||
Keys use a hierarchical array pattern:
|
||||
|
||||
```ts
|
||||
// Resource type → version/qualifier → ID
|
||||
['project', 'v3', projectId]
|
||||
|
||||
// Resource type → ID → sub-resource
|
||||
['project', projectId, 'members']
|
||||
['project', projectId, 'versions', 'v3']
|
||||
|
||||
// Domain → action → ID
|
||||
['backups', 'list', serverId]
|
||||
['tech-reviews']
|
||||
```
|
||||
|
||||
Use `as const` for type safety. Put the resource ID last when possible — this makes partial key matching work for invalidation:
|
||||
|
||||
```ts
|
||||
// Invalidates all project queries for this ID
|
||||
queryClient.invalidateQueries({ queryKey: ['project', projectId] })
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `apps/frontend/src/plugins/tanstack.ts` — QueryClient setup + SSR hydration
|
||||
- `apps/frontend/src/composables/query-client.ts` — `useAppQueryClient()` helper
|
||||
- `apps/frontend/src/composables/queries/` — reusable query option factories
|
||||
@@ -1,6 +1,6 @@
|
||||
name: 👥 Bug with Modrinth Servers
|
||||
description: For issues with a Modrinth Servers product.
|
||||
labels: [servers]
|
||||
name: 👥 Bug with Modrinth Hosting
|
||||
description: For issues with a Modrinth Hosting product.
|
||||
labels: [hosting]
|
||||
type: 'bug'
|
||||
body:
|
||||
- type: checkboxes
|
||||
@@ -19,7 +19,7 @@ body:
|
||||
options:
|
||||
- Modrinth App
|
||||
- Modrinth.com website
|
||||
- Modrinth Servers
|
||||
- Modrinth Hosting
|
||||
- Modrinth API for developers
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -2,50 +2,64 @@
|
||||
applyTo: '**/*.vue'
|
||||
---
|
||||
|
||||
You are given a Nuxt/Vue single-file component (.vue). Your task is to convert every hard-coded natural-language string in the <template> into our localization system using @vintl/vintl-nuxt (which wraps FormatJS).
|
||||
You are given a Nuxt/Vue single-file component (.vue). Your task is to convert every hard-coded natural-language string in the <template> into our localization system using vue-i18n with utilities from `@modrinth/ui`.
|
||||
|
||||
Please follow these rules precisely:
|
||||
|
||||
1. Identify translatable strings
|
||||
|
||||
- Scan the <template> for all user-visible strings (inner text, alt attributes, placeholders, button labels, etc.). Do not extract dynamic expressions (like {{ user.name }}) or HTML tags. Only extract static human-readable text.
|
||||
- There may be strings within the <script> block, e.g dropdown option labels, notifications etc.
|
||||
|
||||
2. Create message definitions
|
||||
|
||||
- In the <script setup> block, import `defineMessage` or `defineMessages` from `@vintl/vintl`.
|
||||
- In the <script setup> block, import `defineMessage` or `defineMessages` from `@modrinth/ui`.
|
||||
- For each extracted string, define a message with a unique `id` (use a descriptive prefix based on the component path, e.g. `auth.welcome.long-title`) and a `defaultMessage` equal to the original English string.
|
||||
Example:
|
||||
const messages = defineMessages({
|
||||
welcomeTitle: { id: 'auth.welcome.title', defaultMessage: 'Welcome' },
|
||||
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: 'You’re now part of the community…' },
|
||||
welcomeDescription: { id: 'auth.welcome.description', defaultMessage: 'You're now part of the community…' },
|
||||
})
|
||||
|
||||
3. Handle variables and ICU formats
|
||||
|
||||
- Replace dynamic parts with ICU placeholders: "Hello, ${user.name}!" → `{name}` and defaultMessage: 'Hello, {name}!'
|
||||
- For numbers/dates/times, use ICU/FormatJS options (e.g., currency): `{price, number, ::currency/USD}`
|
||||
- For numbers/dates/times, use ICU options (e.g., currency): `{price, number, ::currency/USD}`
|
||||
- For plurals/selects, use ICU: `'{count, plural, one {# message} other {# messages}}'`
|
||||
|
||||
4. Rich-text messages (links/markup)
|
||||
|
||||
- In `defaultMessage`, wrap link/markup ranges with tags, e.g.:
|
||||
"By creating an account, you agree to our <terms-link>Terms</terms-link> and <privacy-link>Privacy Policy</privacy-link>."
|
||||
- Render rich-text messages with `<IntlFormatted>` from `@vintl/vintl/components` and map tags via `values`:
|
||||
<IntlFormatted
|
||||
:message="messages.tosLabel"
|
||||
:values="{
|
||||
'terms-link': (chunks) => <NuxtLink to='/terms'>{chunks}</NuxtLink>,
|
||||
'privacy-link': (chunks) => <NuxtLink to='/privacy'>{chunks}</NuxtLink>,
|
||||
}"
|
||||
/>
|
||||
- For simple emphasis: `'Welcome to <strong>Modrinth</strong>!'` and map `'strong': (c) => <strong>{c}</strong>`
|
||||
- Render rich-text messages with `<IntlFormatted>` from `@modrinth/ui` using named slots:
|
||||
<IntlFormatted :message-id="messages.tosLabel">
|
||||
<template #terms-link="{ children }">
|
||||
<NuxtLink to="/terms">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #privacy-link="{ children }">
|
||||
<NuxtLink to="/privacy">
|
||||
<component :is="() => children" />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
- For simple emphasis: `'Welcome to <strong>Modrinth</strong>!'` with a slot:
|
||||
<template #strong="{ children }">
|
||||
<strong><component :is="() => children" /></strong>
|
||||
</template>
|
||||
- For more complex child handling, use `normalizeChildren` from `@modrinth/ui`:
|
||||
<template #bold="{ children }">
|
||||
<strong><component :is="() => normalizeChildren(children)" /></strong>
|
||||
</template>
|
||||
|
||||
5. Formatting in templates
|
||||
|
||||
- Import and use `useVIntl()`; prefer `formatMessage` for simple strings:
|
||||
- Import and use `useVIntl()` from `@modrinth/ui`; prefer `formatMessage` for simple strings:
|
||||
`const { formatMessage } = useVIntl()`
|
||||
`<button>{{ formatMessage(messages.welcomeTitle) }}</button>`
|
||||
- Vue methods like `$formatMessage`, `$formatNumber`, `$formatDate` are also available if needed.
|
||||
- Pass variables as a second argument:
|
||||
`{{ formatMessage(messages.greeting, { name: user.name }) }}`
|
||||
|
||||
6. Naming conventions and id stability
|
||||
|
||||
@@ -57,7 +71,8 @@ Please follow these rules precisely:
|
||||
|
||||
8. Update imports and remove literals
|
||||
|
||||
- Ensure imports for `defineMessage`/`defineMessages`, `useVIntl`, and `<IntlFormatted>` are present. Replace all hard-coded strings with `formatMessage(...)` or `<IntlFormatted>` and remove the literals.
|
||||
- Ensure imports from `@modrinth/ui` are present: `defineMessage`/`defineMessages`, `useVIntl`, `IntlFormatted`, and optionally `normalizeChildren`.
|
||||
- Replace all hard-coded strings with `formatMessage(...)` or `<IntlFormatted>` and remove the literals.
|
||||
|
||||
9. Preserve functionality
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copying
|
||||
|
||||
Modrinth's Github workflows are licensed under the MIT License, which is provided in the file [LICENSE](./LICENSE).
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2025 Rinth, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -13,12 +13,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
- uses: crate-ci/typos@v1.43.1
|
||||
|
||||
# see <https://github.com/influxdata/datafusion-udf-wasm/pull/275>
|
||||
tombi:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: tombi-toml/setup-tombi@v1
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: tombi
|
||||
- run: tombi lint
|
||||
- run: tombi fmt --check
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
name: Deploy frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- prod
|
||||
paths:
|
||||
- 'apps/frontend/**/*'
|
||||
- 'packages/ui/**/*'
|
||||
- 'packages/utils/**/*'
|
||||
- 'packages/assets/**/*'
|
||||
- '**/wrangler.jsonc'
|
||||
- '**/pnpm-*.yaml'
|
||||
- '.github/workflows/frontend-deploy.yml'
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
required: true
|
||||
type: string
|
||||
description: 'The environment to deploy to (staging-preview or production-preview)'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure environment
|
||||
id: meta
|
||||
run: |
|
||||
echo "cmd=deploy" >> $GITHUB_OUTPUT
|
||||
|
||||
ENV_INPUT="${{ inputs.environment }}"
|
||||
REF="${{ github.ref }}"
|
||||
SHA_SHORT="${GITHUB_SHA::8}"
|
||||
|
||||
if [ "$ENV_INPUT" = "staging-preview" ]; then
|
||||
echo "env=staging" >> $GITHUB_OUTPUT
|
||||
echo "url=https://git-$SHA_SHORT-frontend-staging.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true" >> $GITHUB_OUTPUT
|
||||
|
||||
elif [ "$ENV_INPUT" = "production-preview" ]; then
|
||||
echo "env=production" >> $GITHUB_OUTPUT
|
||||
echo "url=https://git-$SHA_SHORT-frontend.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
echo "cmd=versions upload --preview-alias git-$SHA_SHORT --var PREVIEW:true" >> $GITHUB_OUTPUT
|
||||
|
||||
elif [ "$REF" = "refs/heads/main" ]; then
|
||||
echo "env=staging" >> $GITHUB_OUTPUT
|
||||
echo "url=https://staging.modrinth.com" >> $GITHUB_OUTPUT
|
||||
|
||||
else
|
||||
# Production env (no preview)
|
||||
echo "env=production" >> $GITHUB_OUTPUT
|
||||
echo "url=https://modrinth.com" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: Inject build variables
|
||||
working-directory: ./apps/frontend
|
||||
run: |
|
||||
if [ "${{ steps.meta.outputs.env }}" == "staging" ]; then
|
||||
echo "Injecting staging variables from wrangler.jsonc..."
|
||||
jq -r '.env.staging.vars | to_entries[] | "\(.key)=\(.value)"' wrangler.jsonc >> $GITHUB_ENV
|
||||
else
|
||||
echo "Injecting production variables from wrangler.jsonc..."
|
||||
jq -r '.vars | to_entries[] | "\(.key)=\(.value)"' wrangler.jsonc >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./apps/frontend
|
||||
run: pnpm install
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: ./apps/frontend
|
||||
run: pnpm build
|
||||
env:
|
||||
CF_PAGES_BRANCH: ${{ github.ref_name }}
|
||||
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
|
||||
CF_PAGES_URL: ${{ steps.meta.outputs.url }}
|
||||
BUILD_ENV: ${{ steps.meta.outputs.env }}
|
||||
PREVIEW: ${{ inputs.environment != '' && 'true' || 'false' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Create Sentry release and upload sourcemaps
|
||||
uses: getsentry/action-release@v3
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: modrinth
|
||||
SENTRY_PROJECT: knossos-server
|
||||
with:
|
||||
environment: ${{ steps.meta.outputs.env }}
|
||||
sourcemaps: ./apps/frontend/.output/server
|
||||
url_prefix: '~/'
|
||||
|
||||
- name: Deploy Cloudflare Worker
|
||||
id: wrangler
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
environment: ${{ steps.meta.outputs.env != 'production' && steps.meta.outputs.env || '' }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.cmd }}
|
||||
|
||||
- name: Purge cache
|
||||
if: github.ref == 'refs/heads/prod'
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"hosts": ["modrinth.com", "www.modrinth.com", "staging.modrinth.com"]}' \
|
||||
https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache
|
||||
|
||||
- name: Write deployment URL to file
|
||||
if: ${{ inputs.environment != '' }}
|
||||
run: |
|
||||
echo "${{ steps.meta.outputs.url }}" > deployment-url-${{ inputs.environment }}.txt
|
||||
|
||||
- name: Upload deployment URL
|
||||
if: ${{ inputs.environment != '' }}
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: deployment-url-${{ inputs.environment }}
|
||||
path: deployment-url-${{ inputs.environment }}.txt
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Clear pages cache
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- prod
|
||||
|
||||
jobs:
|
||||
wait:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: Cloudflare Pages deployment
|
||||
uses: WalshyDev/cf-pages-await@v1
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: '9ddae624c98677d68d93df6e524a6061'
|
||||
project: 'frontend'
|
||||
commitHash: ${{ steps.push-changes.outputs.commit-hash }}
|
||||
- name: Purge cache
|
||||
if: github.ref == 'refs/heads/prod'
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer ${{ secrets.CF_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data '{"hosts": ["modrinth.com", "www.modrinth.com"]}' \
|
||||
https://api.cloudflare.com/client/v4/zones/e39df17b9c4ef44cbce2646346ee6d33/purge_cache
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Deploy frontend preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'apps/frontend/**/*'
|
||||
- 'packages/ui/**/*'
|
||||
- 'packages/utils/**/*'
|
||||
- 'packages/assets/**/*'
|
||||
- '**/wrangler.jsonc'
|
||||
- '**/pnpm-*.yaml'
|
||||
- '.github/workflows/frontend-preview.yml'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/workflows/frontend-deploy.yml
|
||||
secrets: inherit
|
||||
strategy:
|
||||
matrix:
|
||||
environment: [staging-preview, production-preview]
|
||||
with:
|
||||
environment: ${{ matrix.environment }}
|
||||
|
||||
comment:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Download deployment URLs
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: deployment-url-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Read deployment URLs
|
||||
id: urls
|
||||
run: |
|
||||
STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt)
|
||||
PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt)
|
||||
|
||||
echo "Production preview URL: $PRODUCTION_PREVIEW_URL"
|
||||
echo "Staging preview URL: $STAGING_PREVIEW_URL"
|
||||
|
||||
echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/find-comment@v3
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Frontend previews
|
||||
|
||||
- name: Comment deploy URL on PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
body: |
|
||||
## Frontend previews
|
||||
|
||||
Last deployed commit is [${{ github.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.sha }})
|
||||
|
||||
| Environment | URL |
|
||||
|-------------|-----|
|
||||
| staging | ${{ steps.urls.outputs.staging-preview-url }} |
|
||||
| production | ${{ steps.urls.outputs.production-preview-url }} |
|
||||
edit-mode: replace
|
||||
@@ -19,7 +19,15 @@ on:
|
||||
sign-windows-binaries:
|
||||
description: Sign Windows binaries
|
||||
type: boolean
|
||||
default: true
|
||||
default: false
|
||||
required: false
|
||||
environment:
|
||||
description: Environment
|
||||
type: choice
|
||||
options:
|
||||
- prod
|
||||
- staging
|
||||
default: prod
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
@@ -40,33 +48,48 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: 📥 Check out code
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🧰 Setup Rust toolchain
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ''
|
||||
target: ${{ startsWith(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: 🧰 Install pnpm
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🧰 Setup Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: 🧰 Install Linux build dependencies
|
||||
- name: Generate tauri-dev.conf.json
|
||||
shell: bash
|
||||
run: |
|
||||
GIT_HASH=$(git rev-parse --short HEAD)
|
||||
cat > apps/app/tauri-dev.conf.json <<EOF
|
||||
{
|
||||
"productName": "Modrinth App (dev-${GIT_HASH})",
|
||||
"mainBinaryName": "Modrinth App (dev-${GIT_HASH})",
|
||||
"identifier": "ModrinthApp-dev-${GIT_HASH}",
|
||||
"bundle": {
|
||||
"fileAssociations": []
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: 🧰 Setup Dasel
|
||||
- name: Setup Dasel
|
||||
uses: jaxxstorm/action-install-gh-release@v2.1.0
|
||||
with:
|
||||
repo: TomWright/dasel
|
||||
@@ -75,24 +98,26 @@ jobs:
|
||||
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
chmod: 0755
|
||||
|
||||
- name: ⚙️ Set application version and environment
|
||||
- name: Set application version and environment
|
||||
shell: bash
|
||||
run: |
|
||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||
BUILD_ENVIRONMENT="${{ inputs.environment || 'prod' }}"
|
||||
echo "Setting application version to $APP_VERSION"
|
||||
echo "Using environment $BUILD_ENVIRONMENT"
|
||||
dasel put -f apps/app/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
||||
|
||||
cp packages/app-lib/.env.prod packages/app-lib/.env
|
||||
cp "packages/app-lib/.env.${BUILD_ENVIRONMENT}" packages/app-lib/.env
|
||||
|
||||
- name: 💨 Setup Turbo cache
|
||||
- name: Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@v1.8
|
||||
|
||||
- name: 🧰 Install dependencies
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ✍️ Set up Windows code signing
|
||||
- name: Set up Windows code signing
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -102,8 +127,8 @@ jobs:
|
||||
dasel delete -f apps/app/tauri-release.conf.json 'bundle.windows.signCommand'
|
||||
fi
|
||||
|
||||
- name: 🔨 Build macOS app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||
- name: Build macOS app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
env:
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -116,19 +141,19 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Linux app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
|
||||
- name: Build Linux app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Windows app
|
||||
- name: Build Windows app
|
||||
run: |
|
||||
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
|
||||
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
|
||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
|
||||
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
|
||||
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
env:
|
||||
@@ -138,7 +163,7 @@ jobs:
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: 📤 Upload app bundles
|
||||
- name: Upload app bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: App bundle (${{ matrix.artifact-target-name }})
|
||||
@@ -146,7 +171,13 @@ jobs:
|
||||
target/release/bundle/appimage/Modrinth App_*.AppImage*
|
||||
target/release/bundle/deb/Modrinth App_*.deb*
|
||||
target/release/bundle/rpm/Modrinth App-*.rpm*
|
||||
target/release/bundle/appimage/Modrinth App (dev-*)_*.AppImage*
|
||||
target/release/bundle/deb/Modrinth App (dev-*)_*.deb*
|
||||
target/release/bundle/rpm/Modrinth App (dev-*)-*.rpm*
|
||||
target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz*
|
||||
target/universal-apple-darwin/release/bundle/macos/Modrinth App (dev-*)*.app.tar.gz
|
||||
target/universal-apple-darwin/release/bundle/dmg/Modrinth App (dev-*)*.dmg
|
||||
target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg*
|
||||
target/release/bundle/nsis/Modrinth App_*-setup.exe*
|
||||
target/release/bundle/nsis/Modrinth App (dev-*)_*-setup.exe*
|
||||
target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip*
|
||||
|
||||
@@ -70,23 +70,37 @@ jobs:
|
||||
- name: 🧰 Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ⚙️ Set app environment
|
||||
working-directory: packages/app-lib
|
||||
run: cp .env.staging .env
|
||||
|
||||
# check if labrinth tests will actually run (cache miss)
|
||||
- name: 🔍 Check if labrinth tests need to run
|
||||
id: check-labrinth
|
||||
run: |
|
||||
LABRINTH_TEST_STATUS=$(pnpm turbo run test --filter=@modrinth/labrinth --dry-run=json | jq -r '.tasks[] | select(.task == "test") | .cache.status')
|
||||
echo "Labrinth test cache status: $LABRINTH_TEST_STATUS"
|
||||
if [ "$LABRINTH_TEST_STATUS" = "HIT" ]; then
|
||||
echo "needs_services=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "needs_services=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: ⚙️ Start services
|
||||
if: steps.check-labrinth.outputs.needs_services == 'true'
|
||||
run: docker compose up --wait
|
||||
|
||||
- name: ⚙️ Setup labrinth environment and database
|
||||
if: steps.check-labrinth.outputs.needs_services == 'true'
|
||||
working-directory: apps/labrinth
|
||||
run: |
|
||||
cp .env.local .env
|
||||
sqlx database setup
|
||||
|
||||
- name: ⚙️ Set app environment
|
||||
working-directory: packages/app-lib
|
||||
run: cp .env.staging .env
|
||||
|
||||
- name: 🔍 Lint and test
|
||||
run: pnpm run ci
|
||||
|
||||
- name: 🔍 Verify intl:extract has been run
|
||||
run: |
|
||||
pnpm intl:extract
|
||||
pnpm turbo run intl:extract --force
|
||||
git diff --exit-code --color */*/src/locales/en-US/index.json
|
||||
|
||||
+14
-1
@@ -64,4 +64,17 @@ generated
|
||||
app-playground-data/*
|
||||
|
||||
.astro
|
||||
.claude
|
||||
.claude/*
|
||||
!.claude/skills/
|
||||
.letta
|
||||
|
||||
# labrinth demo fixtures
|
||||
apps/labrinth/fixtures/demo
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
.wrangler/
|
||||
|
||||
# frontend robots.txt
|
||||
apps/frontend/src/public/robots.txt
|
||||
|
||||
Generated
+5
@@ -12,6 +12,11 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/path-util/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-log/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-maxmind/examples" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-maxmind/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/modrinth-util/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/packages/muralpay/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
See [CLAUDE.md](./CLAUDE.md) for all project instructions and guidelines.
|
||||
@@ -1,59 +1,98 @@
|
||||
# Architecture
|
||||
# Modrinth Monorepo
|
||||
|
||||
Use TAB instead of spaces.
|
||||
This is the Modrinth monorepo — it contains all Modrinth projects, both frontend and backend. When entering a project, either to edit or analyse, you should read it's CLAUDE.md.
|
||||
|
||||
## Frontend
|
||||
## Architecture
|
||||
|
||||
There are two similar frontends in the Modrinth monorepo, the website (apps/frontend) and the app frontend (apps/app-frontend).
|
||||
- **Monorepo tooling:** [Turborepo](https://turbo.build/) (`turbo.jsonc`) + [pnpm workspaces](https://pnpm.io/workspaces) (`pnpm-workspace.yaml`)
|
||||
- **Frontend:** Vue 3 / Nuxt 3, Tailwind CSS v3
|
||||
- **Backend:** Rust (Labrinth API), Postgres, Clickhouse
|
||||
- **Indentation:** Use TAB everywhere, never spaces
|
||||
|
||||
Both use Tailwind v3, and their respective configs can be seen at `tailwind.config.ts` and `tailwind.config.js` respectively.
|
||||
### Apps (`apps/`)
|
||||
|
||||
Both utilize shared and common components from `@modrinth/ui` which can be found at `packages/ui`, and stylings from `@modrinth/assets` which can be found at `packages/assets`.
|
||||
| App | Description |
|
||||
| ----------------- | ------------------------------ |
|
||||
| `frontend` | Main Modrinth website (Nuxt 3) |
|
||||
| `app-frontend` | Desktop/app frontend (Vue 3) |
|
||||
| `app` | Desktop/app shell (Tauri) |
|
||||
| `app-playground` | Testing playground for app |
|
||||
| `labrinth` | Backend API service |
|
||||
| `daedalus_client` | Daedalus client implementation |
|
||||
| `docs` | Documentation site (Astro) |
|
||||
|
||||
Both can utilize icons from `@modrinth/assets`, which are automatically generated based on what's available within the `icons` folder of the `packages/assets` directory. You can see the generated icons list in `generated-icons.ts`.
|
||||
### Packages (`packages/`)
|
||||
|
||||
Both have access to our dependency injection framework, examples as seen in `packages/ui/src/providers/`. Ideally any state which is shared between a page and it's subpages should be shared using this dependency injection framework.
|
||||
| Package | Description |
|
||||
| ------------------ | ----------------------------------------------------- |
|
||||
| `ui` | Shared Vue component library (`@modrinth/ui`) |
|
||||
| `assets` | Styling and auto-generated icons (`@modrinth/assets`) |
|
||||
| `api-client` | API client for Nuxt, Tauri, and Node/browser |
|
||||
| `app-lib` | Shared app library |
|
||||
| `blog` | Blog system and changelog data |
|
||||
| `utils` | Shared utility functions |
|
||||
| `moderation` | Moderation utilities |
|
||||
| `daedalus` | Daedalus protocol |
|
||||
| `tooling-config` | ESLint, Prettier, TypeScript configs |
|
||||
| `ariadne` | Analytics library |
|
||||
| `modrinth-log` | Logging utilities |
|
||||
| `modrinth-maxmind` | MaxMind GeoIP |
|
||||
| `modrinth-util` | General utilities |
|
||||
| `muralpay` | Payment processing |
|
||||
| `path-util` | Path utilities |
|
||||
| `sqlx-tracing` | SQLx query tracing |
|
||||
|
||||
### Website (apps/frontend)
|
||||
## Pre-PR Commands
|
||||
|
||||
Before a pull request can be opened for the website, `pnpm web:fix` and `pnpm web:intl:extract` must be run, otherwise CI will fail.
|
||||
Run these from the **root** folder before opening a pull request - do not run these after each prompt the user gives you, only run when asked, ask the user a question if they want to run it if the user indicates that they are about to create a pull request.
|
||||
|
||||
To run a development version of the frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within the `apps/frontend` folder into `apps/frontend/.env`. Then you can run the frontend by running `pnpm web:dev` in the root folder.
|
||||
- **Website:** `pnpm prepr:frontend:web`
|
||||
- **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`
|
||||
|
||||
### App Frontend (apps/app-frontend)
|
||||
The website and app `prepr` commands
|
||||
|
||||
Before a pull request can be opened for the website, you must CD into the `app-frontend` folder; `pnpm fix` and `pnpm intl:extract` must be run, otherwise CI will fail.
|
||||
## Dev Commands
|
||||
|
||||
To run a development version of the app frontend, you must first copy over the relevant `.env` template file (prod, staging or local, usually prod) within `packages/app-lib` into `packages/app-lib/.env`. Then you must run the app itself by running `pnpm app:dev` in the root folder.
|
||||
- **Website:** `pnpm web:dev` (copy `.env` template in `apps/frontend/` first)
|
||||
- **App:** `pnpm app:dev` (copy `.env` template in `packages/app-lib/` first)
|
||||
- **Storybook (packages/ui):** `pnpm storybook`
|
||||
|
||||
### Localization
|
||||
## Project-Specific Instructions
|
||||
|
||||
Refer to `.github/instructions/i18n-convert.instructions.md` if the user asks you to perform any i18n conversion work on a component, set of components, pages or sets of pages.
|
||||
Each project may have its own `CLAUDE.md` with detailed instructions:
|
||||
|
||||
## Labrinth
|
||||
- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API
|
||||
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
|
||||
|
||||
Labrinth is the backend API service for Modrinth.
|
||||
## Code Guidelines
|
||||
|
||||
### Testing
|
||||
### Comments
|
||||
- DO NOT use "heading" comments like: // === Helper methods === .
|
||||
- Use doc comments, but avoid inline comments unless ABSOLUTELY necessary for clarity. Code should aim to be self documenting!
|
||||
|
||||
Before a pull request can be opened, run `cargo clippy -p labrinth --all-targets` and make sure there are ZERO warnings, otherwise CI will fail.
|
||||
## Bash Guidelines
|
||||
|
||||
Use `cargo test -p labrinth --all-targets` to test your changes. All tests must pass, otherwise CI will fail.
|
||||
### Output handling
|
||||
- DO NOT pipe output through `head`, `tail`, `less`, or `more`
|
||||
- NEVER use `| head -n X` or `| tail -n X` to truncate output
|
||||
- Run commands directly without pipes when possible
|
||||
- If you need to limit output, use command-specific flags (e.g. `git log -n 10` instead of `git log | head -10`)
|
||||
- ALWAYS read the full output — never pipe through filters
|
||||
|
||||
To prepare the sqlx cache, cd into `apps/labrinth` and run `cargo sqlx prepare`. Make sure to NEVER run `cargo sqlx prepare --workspace`.
|
||||
### General
|
||||
- Do not create new non-source code files (e.g. Bash scripts, SQL scripts) unless explicitly prompted to
|
||||
|
||||
Read the root `docker-compose.yml` to see what running services are available while developing. Use `docker exec` to access these services.
|
||||
## Skills
|
||||
|
||||
When the user refers to "performing pre-PR checks", do the following:
|
||||
Project-specific skills (patterns, conventions, and implementation guides) are located in [`.claude/skills/`](./.claude/skills/). Each skill has a `SKILL.md` describing the pattern:
|
||||
|
||||
- Run clippy as described above
|
||||
- DO NOT run tests unless explicitly requested (they take a long time)
|
||||
- Prepare the sqlx cache
|
||||
|
||||
### Clickhouse
|
||||
|
||||
Use `docker exec labrinth-clickhouse clickhouse-client` to access the Clickhouse instance. We use the `staging_ariadne` database to store data in testing.
|
||||
|
||||
### Postgres
|
||||
|
||||
Use `docker exec labrinth-postgres psql -U postgres` to access the PostgreSQL instance.
|
||||
- **[Dependency Injection](./.claude/skills/dependency-injection/SKILL.md)** — Vue provide/inject DI layer using `createContext`
|
||||
- **[Cross-Platform Pages](./.claude/skills/cross-platform-pages/SKILL.md)** — Shared component architecture across Nuxt and Tauri frontends
|
||||
- **[Multistage Modals](./.claude/skills/multistage-modals/SKILL.md)** — Wizard-like modal flows with `MultiStageModal`
|
||||
- **[Figma MCP](./.claude/skills/figma-mcp/SKILL.md)** — Translating Figma designs to Modrinth Vue components
|
||||
- **[i18n Convert](./.claude/skills/i18n-convert/SKILL.md)** — Converting hard-coded strings to vue-i18n localization
|
||||
- **[API Module](./.claude/skills/api-module/SKILL.md)** — Adding new endpoint modules to `@modrinth/api-client`
|
||||
- **[TanStack Query](./.claude/skills/tanstack-query/SKILL.md)** — Server state management with `@tanstack/vue-query` v5
|
||||
|
||||
+5
-1
@@ -8,10 +8,14 @@ For detailed information, consult each package's COPYING.md, LICENSE.txt, or LIC
|
||||
|
||||
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
||||
|
||||
> All rights reserved. © 2020-2024 Rinth, Inc.
|
||||
> All rights reserved. © 2020-2025 Rinth, Inc.
|
||||
|
||||
This includes, but may not be limited to, the following files:
|
||||
|
||||
- .idea/icon.svg
|
||||
- .github/api_cover.png
|
||||
- .github/app_cover.png
|
||||
- .github/monorepo_cover.png
|
||||
- .github/web_cover.png
|
||||
|
||||
If you fork this repository, you must remove all Modrinth branding assets from your fork.
|
||||
|
||||
Generated
+1010
-215
File diff suppressed because it is too large
Load Diff
+20
-6
@@ -8,6 +8,8 @@ members = [
|
||||
"packages/app-lib",
|
||||
"packages/ariadne",
|
||||
"packages/daedalus",
|
||||
"packages/labrinth-derive",
|
||||
"packages/modrinth-log",
|
||||
"packages/modrinth-maxmind",
|
||||
"packages/modrinth-util",
|
||||
"packages/path-util",
|
||||
@@ -27,9 +29,11 @@ actix-rt = "2.11.0"
|
||||
actix-web = "4.11.0"
|
||||
actix-web-prom = "0.10.0"
|
||||
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 = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
@@ -54,12 +58,12 @@ clap = "4.5.48"
|
||||
clickhouse = "0.14.0"
|
||||
color-eyre = "0.6.5"
|
||||
color-thief = "0.2.2"
|
||||
console-subscriber = "0.4.1"
|
||||
const_format = "0.2.34"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
darling = { version = "0.23" }
|
||||
dashmap = "6.1.0"
|
||||
data-url = "0.3.2"
|
||||
deadpool-redis = "0.22.0"
|
||||
deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "db5fb00b036ecc8fe5f18853c559b745ffe47bde", version = "0.22.1" }
|
||||
derive_more = "2.0.1"
|
||||
directories = "6.0.0"
|
||||
dirs = "6.0.0"
|
||||
@@ -107,20 +111,24 @@ lettre = { version = "0.11.19", default-features = false, features = [
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||
modrinth-maxmind = { path = "packages/modrinth-maxmind" }
|
||||
modrinth-log = { path = "packages/modrinth-log" }
|
||||
modrinth-util = { path = "packages/modrinth-util" }
|
||||
muralpay = { path = "packages/muralpay" }
|
||||
murmur2 = "0.1.0"
|
||||
native-dialog = "0.9.2"
|
||||
notify = { version = "8.2.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||
p256 = "0.13.2"
|
||||
parking_lot = "0.12.5"
|
||||
paste = "1.0.15"
|
||||
path-util = { path = "packages/path-util" }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
png = "0.18.0"
|
||||
proc-macro2 = { version = "1.0" }
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.38.3"
|
||||
quote = { version = "1.0" }
|
||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
redis = "0.32.7"
|
||||
@@ -139,6 +147,7 @@ rust-s3 = { version = "0.37.0", default-features = false, features = [
|
||||
] }
|
||||
rustls = "0.23.32"
|
||||
rusty-money = "0.4.1"
|
||||
secrecy = "0.10.3"
|
||||
sentry = { version = "0.45.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
@@ -147,7 +156,6 @@ sentry = { version = "0.45.0", default-features = false, features = [
|
||||
"reqwest",
|
||||
"rustls",
|
||||
] }
|
||||
sentry-actix = "0.45.0"
|
||||
serde = "1.0.228"
|
||||
serde_bytes = "0.11.19"
|
||||
serde_cbor = "0.11.2"
|
||||
@@ -158,15 +166,19 @@ serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
shlex = "1.3.0"
|
||||
spdx = "0.12.0"
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
sqlx-tracing = { path = "packages/sqlx-tracing" }
|
||||
strum = "0.27.2"
|
||||
syn = { version = "2.0" }
|
||||
sysinfo = { version = "0.37.2", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tauri = "2.8.5"
|
||||
tauri-build = "2.4.1"
|
||||
tauri-plugin-deep-link = "2.4.3"
|
||||
tauri-plugin-dialog = "2.4.0"
|
||||
tauri-plugin-http = "2.5.2"
|
||||
tauri-plugin-http = "2.5.7"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri-plugin-os = "2.3.1"
|
||||
tauri-plugin-single-instance = "2.3.4"
|
||||
@@ -234,7 +246,7 @@ manual_assert = "warn"
|
||||
manual_instant_elapsed = "warn"
|
||||
manual_is_variant_and = "warn"
|
||||
manual_let_else = "warn"
|
||||
map_unwrap_or = "warn"
|
||||
map_unwrap_or = "allow"
|
||||
match_bool = "warn"
|
||||
needless_collect = "warn"
|
||||
negative_feature_names = "warn"
|
||||
@@ -245,6 +257,7 @@ redundant_clone = "warn"
|
||||
redundant_feature_names = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
todo = "warn"
|
||||
too_many_arguments = "allow"
|
||||
uninlined_format_args = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
@@ -263,4 +276,5 @@ codegen-units = 1 # Compile crates one after another so the compiler can optimi
|
||||
# Specific profile for labrinth production builds
|
||||
[profile.release-labrinth]
|
||||
inherits = "release"
|
||||
strip = false # Keep debug symbols for Sentry
|
||||
panic = "unwind" # Don't exit the whole app on panic in production
|
||||
|
||||
+5
-1
@@ -9,7 +9,7 @@ extend-exclude = [
|
||||
# contains licenses like `CC-BY-ND-4.0`
|
||||
"packages/moderation/src/data/stages/license.ts",
|
||||
# contains payment card IDs like `IY1VMST1MOXS` which are flagged
|
||||
"apps/labrinth/src/queue/payouts.rs",
|
||||
"apps/labrinth/src/queue/payouts/mod.rs",
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
@@ -17,3 +17,7 @@ extend-exclude = [
|
||||
tou = "tou"
|
||||
# Google Ad Manager
|
||||
gam = "gam"
|
||||
# short for "constants"
|
||||
consts = "consts"
|
||||
# short for "Copy"
|
||||
Cpy = "Cpy"
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
# Copying
|
||||
|
||||
The source code of the theseus repository is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
|
||||
The source code of Modrinth App's frontend is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](./LICENSE). However, some files listed below are licensed under a different license.
|
||||
|
||||
## Modrinth logo
|
||||
|
||||
The use of Modrinth branding elements, including but not limited to the wrench-in-labyrinth logo, the landing image, and any variations thereof, is strictly prohibited without explicit written permission from Rinth, Inc. This includes trademarks, logos, or other branding elements.
|
||||
|
||||
> All rights reserved. © 2020-2023 Rinth, Inc.
|
||||
|
||||
This includes, but may not be limited to, the following files:
|
||||
|
||||
- theseus_gui/src-tauri/icons/\*
|
||||
> All rights reserved. © 2020-2025 Rinth, Inc.
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
"test": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sfirew/minecraft-motd-parser": "^1.1.6",
|
||||
"@modrinth/api-client": "workspace:^",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@sfirew/minecraft-motd-parser": "^1.1.6",
|
||||
"@tanstack/vue-query": "^5.90.7",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.0",
|
||||
@@ -26,27 +28,28 @@
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"intl-messageformat": "^10.7.7",
|
||||
"vue-i18n": "^10.0.0",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
"ofetch": "^1.3.4",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia": "^3.0.0",
|
||||
"posthog-js": "^1.158.2",
|
||||
"three": "^0.172.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-multiselect": "3.0.0",
|
||||
"vue-router": "4.3.0",
|
||||
"vue-router": "^4.6.0",
|
||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"@nuxt/eslint-config": "^0.5.6",
|
||||
"@taijased/vue-render-tracker": "^1.0.7",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-turbo": "^2.5.4",
|
||||
@@ -55,7 +58,8 @@
|
||||
"sass": "^1.74.1",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.6",
|
||||
"vite": "^6.0.0",
|
||||
"vue-component-type-helpers": "^3.1.8",
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
|
||||
+289
-117
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import { AuthFeature, PanelVersionFeature, TauriModrinthClient } from '@modrinth/api-client'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChangeSkinIcon,
|
||||
@@ -18,30 +19,40 @@ import {
|
||||
RefreshCwIcon,
|
||||
RestoreIcon,
|
||||
RightArrowIcon,
|
||||
ServerIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
WorldIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
Button,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
I18nDebugPanel,
|
||||
NewsArticleCard,
|
||||
NotificationPanel,
|
||||
OverflowMenu,
|
||||
PopupNotificationPanel,
|
||||
ProgressSpinner,
|
||||
provideModrinthClient,
|
||||
provideNotificationManager,
|
||||
providePageContext,
|
||||
providePopupNotificationManager,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import { formatBytes, renderString } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { $fetch } from 'ofetch'
|
||||
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
@@ -52,25 +63,28 @@ import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
|
||||
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
|
||||
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
||||
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
|
||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
||||
import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
|
||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import UpdateToast from '@/components/ui/UpdateToast.vue'
|
||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
@@ -83,18 +97,20 @@ import {
|
||||
isDev,
|
||||
isNetworkMetered,
|
||||
} from '@/helpers/utils.js'
|
||||
import i18n from '@/i18n.config'
|
||||
import {
|
||||
provideAppUpdateDownloadProgress,
|
||||
subscribeToDownloadProgress,
|
||||
} from '@/providers/download-progress.ts'
|
||||
import { useError } from '@/store/error.js'
|
||||
import { useInstall } from '@/store/install.js'
|
||||
import { playServerProject, useInstall } from '@/store/install.js'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
|
||||
import { create_profile_and_install_from_file } from './helpers/pack'
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
import { AppNotificationManager } from './providers/app-notifications'
|
||||
import { AppPopupNotificationManager } from './providers/app-popup-notifications'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -102,6 +118,24 @@ const notificationManager = new AppNotificationManager()
|
||||
provideNotificationManager(notificationManager)
|
||||
const { handleError, addNotification } = notificationManager
|
||||
|
||||
const popupNotificationManager = new AppPopupNotificationManager()
|
||||
providePopupNotificationManager(popupNotificationManager)
|
||||
const { addPopupNotification } = popupNotificationManager
|
||||
|
||||
const tauriApiClient = new TauriModrinthClient({
|
||||
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
|
||||
features: [
|
||||
new AuthFeature({
|
||||
token: async () => (await getCreds()).session,
|
||||
}),
|
||||
new PanelVersionFeature(),
|
||||
],
|
||||
})
|
||||
provideModrinthClient(tauriApiClient)
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(true),
|
||||
showAds: ref(false),
|
||||
})
|
||||
const news = ref([])
|
||||
const availableSurvey = ref(false)
|
||||
|
||||
@@ -119,6 +153,7 @@ const showOnboarding = ref(false)
|
||||
const nativeDecorations = ref(false)
|
||||
|
||||
const os = ref('')
|
||||
const isDevEnvironment = ref(false)
|
||||
|
||||
const stateInitialized = ref(false)
|
||||
|
||||
@@ -126,6 +161,27 @@ const criticalErrorMessage = ref()
|
||||
|
||||
const isMaximized = ref(false)
|
||||
|
||||
const authUnreachableDebug = useDebugLogger('AuthReachableChecker')
|
||||
const authServerQuery = useQuery({
|
||||
queryKey: ['authServerReachability'],
|
||||
queryFn: async () => {
|
||||
await check_reachable()
|
||||
authUnreachableDebug('Auth servers are reachable')
|
||||
return true
|
||||
},
|
||||
refetchInterval: 5 * 60 * 1000, // 5 minutes
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
const authUnreachable = computed(() => {
|
||||
if (authServerQuery.isError.value && !authServerQuery.isLoading.value) {
|
||||
console.warn('Failed to reach auth servers', authServerQuery.error.value)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await useCheckDisableMouseover()
|
||||
|
||||
@@ -164,13 +220,22 @@ const messages = defineMessages({
|
||||
id: 'app.update.downloading-update',
|
||||
defaultMessage: 'Downloading update ({percent}%)',
|
||||
},
|
||||
authUnreachableHeader: {
|
||||
id: 'app.auth-servers.unreachable.header',
|
||||
defaultMessage: 'Cannot reach authentication servers',
|
||||
},
|
||||
authUnreachableBody: {
|
||||
id: 'app.auth-servers.unreachable.body',
|
||||
defaultMessage:
|
||||
'Minecraft authentication servers may be down right now. Check your internet connection and try again later.',
|
||||
},
|
||||
})
|
||||
|
||||
async function setupApp() {
|
||||
stateInitialized.value = true
|
||||
const {
|
||||
native_decorations,
|
||||
theme,
|
||||
locale,
|
||||
telemetry,
|
||||
collapsed_navigation,
|
||||
advanced_rendering,
|
||||
@@ -182,12 +247,18 @@ async function setupApp() {
|
||||
pending_update_toast_for_version,
|
||||
} = await getSettings()
|
||||
|
||||
// Initialize locale from saved settings
|
||||
if (locale) {
|
||||
i18n.global.locale.value = locale
|
||||
}
|
||||
|
||||
if (default_page === 'Library') {
|
||||
await router.push('/library')
|
||||
}
|
||||
|
||||
os.value = await getOS()
|
||||
const dev = await isDev()
|
||||
isDevEnvironment.value = dev
|
||||
const version = await getVersion()
|
||||
showOnboarding.value = !onboarded
|
||||
|
||||
@@ -200,6 +271,7 @@ async function setupApp() {
|
||||
themeStore.toggleSidebar = toggle_sidebar
|
||||
themeStore.devMode = developer_mode
|
||||
themeStore.featureFlags = feature_flags
|
||||
stateInitialized.value = true
|
||||
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
|
||||
@@ -207,12 +279,11 @@ async function setupApp() {
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
})
|
||||
|
||||
initAnalytics()
|
||||
if (!telemetry) {
|
||||
optOutAnalytics()
|
||||
if (telemetry) {
|
||||
initAnalytics()
|
||||
if (dev) debugAnalytics()
|
||||
trackEvent('Launched', { version, dev, onboarded })
|
||||
}
|
||||
if (dev) debugAnalytics()
|
||||
trackEvent('Launched', { version, dev, onboarded })
|
||||
|
||||
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
|
||||
|
||||
@@ -231,11 +302,7 @@ async function setupApp() {
|
||||
}),
|
||||
)
|
||||
|
||||
useFetch(
|
||||
`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
|
||||
'criticalAnnouncements',
|
||||
true,
|
||||
)
|
||||
fetch(`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
if (res && res.header && res.body) {
|
||||
@@ -248,23 +315,21 @@ async function setupApp() {
|
||||
)
|
||||
})
|
||||
|
||||
useFetch(`https://modrinth.com/news/feed/articles.json`, 'news', true)
|
||||
fetch(`https://modrinth.com/news/feed/articles.json`)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
if (res && res.articles) {
|
||||
// Format expected by NewsArticleCard component.
|
||||
news.value = res.articles
|
||||
.map((article) => ({
|
||||
...article,
|
||||
path: article.link,
|
||||
thumbnail: article.thumbnail,
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
date: article.date,
|
||||
}))
|
||||
.slice(0, 4)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch news articles', error)
|
||||
})
|
||||
|
||||
get_opening_command().then(handleCommand)
|
||||
fetchCredentials()
|
||||
@@ -312,7 +377,11 @@ const handleClose = async () => {
|
||||
|
||||
const router = useRouter()
|
||||
router.afterEach((to, from, failure) => {
|
||||
trackEvent('PageView', { path: to.path, fromPath: from.path, failed: failure })
|
||||
trackEvent('PageView', {
|
||||
path: to.path,
|
||||
fromPath: from.path,
|
||||
failed: failure,
|
||||
})
|
||||
})
|
||||
const route = useRoute()
|
||||
|
||||
@@ -321,11 +390,15 @@ loading.setEnabled(false)
|
||||
|
||||
const error = useError()
|
||||
const errorModal = ref()
|
||||
const minecraftAuthErrorModal = ref()
|
||||
|
||||
const install = useInstall()
|
||||
const modInstallModal = ref()
|
||||
const addServerToInstanceModal = ref()
|
||||
const installConfirmModal = ref()
|
||||
const incompatibilityWarningModal = ref()
|
||||
const installToPlayModal = ref()
|
||||
const updateToPlayModal = ref()
|
||||
|
||||
const credentials = ref()
|
||||
|
||||
@@ -334,9 +407,9 @@ const modrinthLoginFlowWaitModal = ref()
|
||||
async function fetchCredentials() {
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (creds && creds.user_id) {
|
||||
creds.user = await get_user(creds.user_id).catch(handleError)
|
||||
creds.user = await get_user(creds.user_id, 'bypass').catch(handleError)
|
||||
}
|
||||
credentials.value = creds
|
||||
credentials.value = creds ?? null
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
@@ -383,28 +456,31 @@ const forceSidebar = computed(
|
||||
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
|
||||
)
|
||||
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
||||
const showAd = computed(() => !(!sidebarVisible.value || hasPlus.value))
|
||||
|
||||
watch(
|
||||
showAd,
|
||||
() => {
|
||||
if (!showAd.value) {
|
||||
hide_ads_window(true)
|
||||
} else {
|
||||
init_ads_window(true)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
const showAd = computed(
|
||||
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
|
||||
)
|
||||
|
||||
watch(showAd, () => {
|
||||
if (!showAd.value) {
|
||||
hide_ads_window(true)
|
||||
} else {
|
||||
init_ads_window(true)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
invoke('show_window')
|
||||
|
||||
error.setErrorModal(errorModal.value)
|
||||
error.setMinecraftAuthErrorModal(minecraftAuthErrorModal.value)
|
||||
|
||||
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
||||
install.setInstallConfirmModal(installConfirmModal)
|
||||
install.setModInstallModal(modInstallModal)
|
||||
install.setAddServerToInstanceModal(addServerToInstanceModal)
|
||||
install.setInstallToPlayModal(installToPlayModal)
|
||||
install.setUpdateToPlayModal(updateToPlayModal)
|
||||
install.setPopupNotificationManager(popupNotificationManager)
|
||||
})
|
||||
|
||||
const accounts = ref(null)
|
||||
@@ -422,6 +498,9 @@ async function handleCommand(e) {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
}
|
||||
} else if (e.event === 'InstallServer') {
|
||||
await router.push(`/project/${e.id}`)
|
||||
await playServerProject(e.id).catch(handleError)
|
||||
} else {
|
||||
// Other commands are URL-based (deep linking)
|
||||
urlModal.value.show(e)
|
||||
@@ -440,24 +519,72 @@ const downloadPercent = computed(() => Math.trunc(appUpdateDownload.progress.val
|
||||
const metered = ref(true)
|
||||
const finishedDownloading = ref(false)
|
||||
const restarting = ref(false)
|
||||
const updateToastDismissed = ref(false)
|
||||
const availableUpdate = ref(null)
|
||||
const updateSize = ref(null)
|
||||
const updatesEnabled = ref(true)
|
||||
|
||||
const updatePopupMessages = defineMessages({
|
||||
updateAvailable: {
|
||||
id: 'app.update-popup.title',
|
||||
defaultMessage: 'Update available',
|
||||
},
|
||||
downloadComplete: {
|
||||
id: 'app.update-popup.download-complete',
|
||||
defaultMessage: 'Download complete',
|
||||
},
|
||||
body: {
|
||||
id: 'app.update-popup.body',
|
||||
defaultMessage:
|
||||
'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.',
|
||||
},
|
||||
meteredBody: {
|
||||
id: 'app.update-popup.body.metered',
|
||||
defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`,
|
||||
},
|
||||
downloadedBody: {
|
||||
id: 'app.update-popup.body.download-complete',
|
||||
defaultMessage: `Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App.`,
|
||||
},
|
||||
linuxBody: {
|
||||
id: 'app.update-popup.body.linux',
|
||||
defaultMessage:
|
||||
'Modrinth App v{version} is available. Use your package manager to update for the latest features and fixes!',
|
||||
},
|
||||
reload: {
|
||||
id: 'app.update-popup.reload',
|
||||
defaultMessage: 'Reload',
|
||||
},
|
||||
download: {
|
||||
id: 'app.update-popup.download',
|
||||
defaultMessage: 'Download ({size})',
|
||||
},
|
||||
changelog: {
|
||||
id: 'app.update-popup.changelog',
|
||||
defaultMessage: 'Changelog',
|
||||
},
|
||||
})
|
||||
|
||||
async function checkUpdates() {
|
||||
if (!(await areUpdatesEnabled())) {
|
||||
console.log('Skipping update check as updates are disabled in this build or environment')
|
||||
updatesEnabled.value = false
|
||||
|
||||
if (os.value === 'Linux' && !isDevEnvironment.value) {
|
||||
checkLinuxUpdates()
|
||||
setInterval(checkLinuxUpdates, 5 * 60 * 1000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
async function performCheck() {
|
||||
const update = await invoke('plugin:updater|check')
|
||||
const isExistingUpdate = update.version === availableUpdate.value?.version
|
||||
|
||||
if (!update) {
|
||||
console.log('No update available')
|
||||
return
|
||||
}
|
||||
|
||||
const isExistingUpdate = update.version === availableUpdate.value?.version
|
||||
|
||||
if (isExistingUpdate) {
|
||||
console.log('Update is already known')
|
||||
return
|
||||
@@ -465,7 +592,6 @@ async function checkUpdates() {
|
||||
|
||||
appUpdateDownload.progress.value = 0
|
||||
finishedDownloading.value = false
|
||||
updateToastDismissed.value = false
|
||||
|
||||
console.log(`Update ${update.version} is available.`)
|
||||
|
||||
@@ -475,6 +601,28 @@ async function checkUpdates() {
|
||||
downloadUpdate(update)
|
||||
} else {
|
||||
console.log(`Metered connection detected, not auto-downloading update.`)
|
||||
getUpdateSize(update.rid).then((size) => {
|
||||
updateSize.value = size
|
||||
addPopupNotification({
|
||||
title: formatMessage(updatePopupMessages.updateAvailable),
|
||||
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
|
||||
type: 'info',
|
||||
autoCloseMs: null,
|
||||
buttons: [
|
||||
{
|
||||
label: formatMessage(updatePopupMessages.download, {
|
||||
size: formatBytes(updateSize.value ?? 0),
|
||||
}),
|
||||
action: () => downloadAvailableUpdate(),
|
||||
color: 'brand',
|
||||
},
|
||||
{
|
||||
label: formatMessage(updatePopupMessages.changelog),
|
||||
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getUpdateSize(update.rid).then((size) => (updateSize.value = size))
|
||||
@@ -491,8 +639,26 @@ async function checkUpdates() {
|
||||
)
|
||||
}
|
||||
|
||||
async function showUpdateToast() {
|
||||
updateToastDismissed.value = false
|
||||
async function checkLinuxUpdates() {
|
||||
try {
|
||||
const [response, currentVersion] = await Promise.all([
|
||||
fetch('https://launcher-files.modrinth.com/updates.json'),
|
||||
getVersion(),
|
||||
])
|
||||
const updates = await response.json()
|
||||
const latestVersion = updates?.version
|
||||
|
||||
if (latestVersion && latestVersion !== currentVersion) {
|
||||
addPopupNotification({
|
||||
title: formatMessage(updatePopupMessages.updateAvailable),
|
||||
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
|
||||
type: 'info',
|
||||
autoCloseMs: null,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to check for updates:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAvailableUpdate() {
|
||||
@@ -518,6 +684,26 @@ async function downloadUpdate(versionToDownload) {
|
||||
unlistenUpdateDownload = null
|
||||
})
|
||||
console.log('Finished downloading!')
|
||||
|
||||
addPopupNotification({
|
||||
title: formatMessage(updatePopupMessages.downloadComplete),
|
||||
text: formatMessage(updatePopupMessages.downloadedBody, {
|
||||
version: versionToDownload.version,
|
||||
}),
|
||||
type: 'success',
|
||||
autoCloseMs: null,
|
||||
buttons: [
|
||||
{
|
||||
label: formatMessage(updatePopupMessages.reload),
|
||||
action: () => installUpdate(),
|
||||
color: 'brand',
|
||||
},
|
||||
{
|
||||
label: formatMessage(updatePopupMessages.changelog),
|
||||
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
unlistenUpdateDownload = await subscribeToDownloadProgress(
|
||||
appUpdateDownload,
|
||||
@@ -686,25 +872,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<template>
|
||||
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
|
||||
<div id="teleports"></div>
|
||||
<div v-if="stateInitialized" class="app-grid-layout experimental-styles-within relative">
|
||||
<Suspense>
|
||||
<Transition name="toast">
|
||||
<UpdateToast
|
||||
v-if="
|
||||
!!availableUpdate &&
|
||||
!updateToastDismissed &&
|
||||
!restarting &&
|
||||
(finishedDownloading || metered)
|
||||
"
|
||||
:version="availableUpdate.version"
|
||||
:size="updateSize"
|
||||
:metered="metered"
|
||||
@close="updateToastDismissed = true"
|
||||
@restart="installUpdate"
|
||||
@download="downloadAvailableUpdate"
|
||||
/>
|
||||
</Transition>
|
||||
</Suspense>
|
||||
<div
|
||||
v-if="stateInitialized"
|
||||
class="app-grid-layout experimental-styles-within relative"
|
||||
:class="{ 'disable-advanced-rendering': !themeStore.advancedRendering }"
|
||||
>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="restarting"
|
||||
@@ -738,6 +910,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<NavButton v-if="themeStore.featureFlags.worlds_tab" v-tooltip.right="'Worlds'" to="/worlds">
|
||||
<WorldIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-if="themeStore.featureFlags.servers_in_app"
|
||||
v-tooltip.right="'Servers'"
|
||||
to="/hosting/manage"
|
||||
>
|
||||
<ServerIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Discover content'"
|
||||
to="/browse/modpack"
|
||||
@@ -761,7 +940,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
>
|
||||
<LibraryIcon />
|
||||
</NavButton>
|
||||
<div class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||
<div class="h-px w-6 mx-auto my-2 bg-surface-5"></div>
|
||||
<suspense>
|
||||
<QuickInstanceSwitcher />
|
||||
</suspense>
|
||||
@@ -774,14 +953,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</NavButton>
|
||||
<div class="flex flex-grow"></div>
|
||||
<Transition name="nav-button-animated">
|
||||
<div
|
||||
v-if="
|
||||
availableUpdate &&
|
||||
updateToastDismissed &&
|
||||
!restarting &&
|
||||
(finishedDownloading || metered)
|
||||
"
|
||||
>
|
||||
<div v-if="availableUpdate && !restarting && (finishedDownloading || metered)">
|
||||
<NavButton
|
||||
v-tooltip.right="
|
||||
formatMessage(
|
||||
@@ -795,13 +967,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
},
|
||||
)
|
||||
"
|
||||
:to="
|
||||
finishedDownloading
|
||||
? installUpdate
|
||||
: downloadProgress > 0 && downloadProgress < 1
|
||||
? showUpdateToast
|
||||
: downloadAvailableUpdate
|
||||
"
|
||||
:to="finishedDownloading ? installUpdate : downloadAvailableUpdate"
|
||||
>
|
||||
<ProgressSpinner
|
||||
v-if="downloadProgress > 0 && downloadProgress < 1"
|
||||
@@ -820,7 +986,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<SettingsIcon />
|
||||
</NavButton>
|
||||
<OverflowMenu
|
||||
v-if="credentials"
|
||||
v-if="credentials?.user"
|
||||
v-tooltip.right="`Modrinth account`"
|
||||
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast border-0 cursor-pointer"
|
||||
:options="[
|
||||
@@ -836,14 +1002,14 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
]"
|
||||
placement="right-end"
|
||||
>
|
||||
<Avatar :src="credentials.user.avatar_url" alt="" size="32px" circle />
|
||||
<Avatar :src="credentials?.user?.avatar_url" alt="" size="32px" circle />
|
||||
<template #view-profile>
|
||||
<UserIcon />
|
||||
<span class="inline-flex items-center gap-1">
|
||||
Signed in as
|
||||
<span class="inline-flex items-center gap-1 text-contrast font-semibold">
|
||||
<Avatar :src="credentials.user.avatar_url" alt="" size="20px" circle />
|
||||
{{ credentials.user.username }}
|
||||
<Avatar :src="credentials?.user?.avatar_url" alt="" size="20px" circle />
|
||||
{{ credentials?.user?.username }}
|
||||
</span>
|
||||
</span>
|
||||
<ExternalIcon />
|
||||
@@ -914,13 +1080,16 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<div
|
||||
v-if="stateInitialized"
|
||||
class="app-contents experimental-styles-within"
|
||||
:class="{ 'sidebar-enabled': sidebarVisible }"
|
||||
:class="{
|
||||
'sidebar-enabled': sidebarVisible,
|
||||
'disable-advanced-rendering': !themeStore.advancedRendering,
|
||||
}"
|
||||
>
|
||||
<div class="app-viewport flex-grow router-view">
|
||||
<transition name="popup-survey">
|
||||
<div
|
||||
v-if="availableSurvey"
|
||||
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-divider border-[1px] border-solid border-b-0 p-4"
|
||||
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-surface-5 border-[1px] border-solid border-b-0 p-4"
|
||||
>
|
||||
<h2 class="text-lg font-extrabold mt-0 mb-2">Hey there Modrinth user!</h2>
|
||||
<p class="m-0 leading-tight">
|
||||
@@ -962,16 +1131,25 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
width: 'calc(100% - var(--right-bar-width))',
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
<Admonition
|
||||
v-if="criticalErrorMessage"
|
||||
class="m-6 mb-0 flex flex-col border-red bg-bg-red rounded-2xl border-2 border-solid p-4 gap-1 font-semibold text-contrast"
|
||||
type="critical"
|
||||
:header="criticalErrorMessage.header"
|
||||
class="m-6 mb-0"
|
||||
>
|
||||
<h1 class="m-0 text-lg font-extrabold">{{ criticalErrorMessage.header }}</h1>
|
||||
<div
|
||||
class="markdown-body text-primary"
|
||||
v-html="renderString(criticalErrorMessage.body ?? '')"
|
||||
></div>
|
||||
</div>
|
||||
</Admonition>
|
||||
<Admonition
|
||||
v-if="authUnreachable"
|
||||
type="warning"
|
||||
:header="formatMessage(messages.authUnreachableHeader)"
|
||||
class="m-6 mb-0"
|
||||
>
|
||||
{{ formatMessage(messages.authUnreachableBody) }}
|
||||
</Admonition>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
|
||||
@@ -1037,11 +1215,17 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</div>
|
||||
</div>
|
||||
<URLConfirmModal ref="urlModal" />
|
||||
<I18nDebugPanel />
|
||||
<NotificationPanel has-sidebar />
|
||||
<PopupNotificationPanel has-sidebar />
|
||||
<ErrorModal ref="errorModal" />
|
||||
<MinecraftAuthErrorModal ref="minecraftAuthErrorModal" />
|
||||
<ModInstallModal ref="modInstallModal" />
|
||||
<AddServerToInstanceModal ref="addServerToInstanceModal" />
|
||||
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
||||
<InstallConfirmModal ref="installConfirmModal" />
|
||||
<InstallToPlayModal ref="installToPlayModal" />
|
||||
<UpdateToPlayModal ref="updateToPlayModal" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -1194,9 +1378,19 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disable-advanced-rendering {
|
||||
.app-sidebar::before {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.app-contents::before {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-sidebar::before {
|
||||
content: '';
|
||||
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.2) inset;
|
||||
box-shadow: -15px 0 15px -15px rgba(0, 0, 0, 0.1) inset;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -2rem;
|
||||
@@ -1221,9 +1415,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
right: calc(-1 * var(--left-bar-width));
|
||||
bottom: calc(-1 * var(--left-bar-width));
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow:
|
||||
1px 1px 15px rgba(0, 0, 0, 0.2) inset,
|
||||
inset 1px 1px 1px rgba(255, 255, 255, 0.23);
|
||||
box-shadow: 1px 1px 15px rgba(0, 0, 0, 0.1) inset;
|
||||
border-color: var(--surface-5);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1259,38 +1454,15 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
transform: translateY(10rem) scale(0.8) scaleY(1.6);
|
||||
}
|
||||
|
||||
.toast-enter-active {
|
||||
transition: opacity 0.25s linear;
|
||||
}
|
||||
|
||||
.toast-enter-from,
|
||||
.toast-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.toast-enter-active,
|
||||
.nav-button-animated-enter-active {
|
||||
transition: all 0.5s cubic-bezier(0.15, 1.4, 0.64, 0.96);
|
||||
}
|
||||
|
||||
.toast-leave-active,
|
||||
.nav-button-animated-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.toast-enter-from {
|
||||
scale: 0.5;
|
||||
translate: 0 -10rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.toast-leave-to {
|
||||
scale: 0.96;
|
||||
translate: 20rem 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nav-button-animated-enter-active {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1354,7 +1526,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
}
|
||||
|
||||
.info-card {
|
||||
right: 8rem;
|
||||
right: 22rem;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:serif="http://www.serif.com/" version="1.1" viewBox="0 0 1793 199">
|
||||
<g>
|
||||
<g id="Layer_1">
|
||||
<g id="green" fill="var(--color-brand)">
|
||||
<path d="M1184.1,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
||||
<path d="M1291.1,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
||||
<path d="M1357.2,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
||||
<path d="M1460,165.3l-40.8-95.1h23.2l35.1,83.9h-11.4l36.3-83.9h21.4l-40.8,95.1h-23Z"/>
|
||||
<path d="M1579.6,166.6c-10.6,0-19.8-2.1-27.7-6.3-7.9-4.2-14-10-18.3-17.4-4.3-7.4-6.5-15.7-6.5-25.1s2.1-17.9,6.3-25.2c4.2-7.3,10-13,17.5-17.2,7.4-4.2,15.9-6.2,25.4-6.2s17.5,2,24.8,6.1c7.2,4,12.9,9.7,17.1,17.1,4.2,7.4,6.2,16,6.2,26s0,2,0,3.2c0,1.2-.2,2.3-.3,3.4h-79.3v-14.8h67.5l-8.7,4.6c.1-5.5-1-10.3-3.4-14.4-2.4-4.2-5.6-7.4-9.7-9.8-4.1-2.4-8.8-3.6-14.2-3.6s-10.2,1.2-14.3,3.6c-4.1,2.4-7.3,5.7-9.6,9.9-2.3,4.2-3.5,9.2-3.5,14.9v3.6c0,5.7,1.3,10.7,3.9,15.1,2.6,4.4,6.3,7.8,11,10.2,4.7,2.4,10.2,3.6,16.4,3.6s10.2-.8,14.4-2.5c4.3-1.7,8.1-4.3,11.4-7.8l11.9,13.7c-4.3,5-9.6,8.8-16.1,11.5-6.5,2.7-13.9,4-22.2,4Z"/>
|
||||
<path d="M1645.7,165.3v-95.1h21.2v26.2l-2.5-7.7c2.8-6.4,7.3-11.3,13.4-14.6,6.1-3.3,13.7-5,22.9-5v21.2c-1-.2-1.8-.4-2.7-.4-.8,0-1.7,0-2.5,0-8.4,0-15.1,2.5-20.1,7.4-5,4.9-7.5,12.3-7.5,22v46.1h-22.3Z"/>
|
||||
<path d="M1749.9,166.6c-8,0-15.6-1-22.9-3.1s-13.1-4.6-17.4-7.6l8.5-16.9c4.3,2.7,9.4,5,15.3,6.8,5.9,1.8,11.9,2.7,17.8,2.7s12.1-.9,15.2-2.8c3.1-1.9,4.7-4.5,4.7-7.7s-1.1-4.6-3.2-6c-2.1-1.4-4.9-2.4-8.4-3.1-3.4-.7-7.3-1.4-11.5-2-4.2-.6-8.4-1.4-12.6-2.4-4.2-1-8-2.5-11.5-4.5-3.4-2-6.2-4.6-8.4-7.9-2.1-3.3-3.2-7.7-3.2-13.2s1.7-11.3,5.2-15.8c3.4-4.5,8.3-7.9,14.5-10.3,6.2-2.4,13.6-3.7,22.2-3.7s12.9.7,19.4,2.1c6.5,1.4,11.9,3.4,16.2,6.1l-8.5,16.9c-4.5-2.7-9.1-4.6-13.6-5.6-4.6-1-9.1-1.5-13.6-1.5-6.8,0-11.8,1-15,3-3.3,2-4.9,4.6-4.9,7.7s1.1,5,3.2,6.4c2.1,1.4,4.9,2.6,8.4,3.4,3.4.8,7.3,1.5,11.5,2,4.2.5,8.4,1.3,12.6,2.4,4.2,1.1,8,2.5,11.5,4.4,3.5,1.8,6.3,4.4,8.5,7.7,2.1,3.3,3.2,7.7,3.2,13s-1.8,11.1-5.3,15.5c-3.5,4.4-8.5,7.8-14.9,10.2-6.4,2.4-14.1,3.7-23,3.7Z"/>
|
||||
<g>
|
||||
<path d="M9.8,143l63.4-38.1-5.8-15.3,18.1-18.6,22.9-4.9,6.6,8.2-10.6,10.7-9.2,2.9-6.6,6.8,3.2,9,6.5,6.9,9.2-2.5,6.6-7.2,14.3-4.5,4.3,9.6-14.8,18.1-24.8,7.8-11.1-12.4-63.6,38.2c-3-3.9-6.5-9.4-8.8-14.7ZM192.8,65.4l-50.4,13.6c2.8,7.4,3.7,11.7,4.5,16.5l50.3-13.6c-.8-5.4-2.2-10.8-4.4-16.5Z" fill-rule="evenodd"/>
|
||||
<path d="M17.3,106.5c3.6,42.1,38.9,75.2,82,75.2s60.7-18.9,74-46.3l16.4,5.7c-15.8,34.1-50.3,57.9-90.4,57.9S3.6,158.2,0,106.5h17.3ZM.3,89.4C5.3,39.2,47.8,0,99.3,0s99.5,44.6,99.5,99.5-1.1,17.4-3.3,25.5l-16.3-5.7c1.6-6.5,2.4-13.1,2.4-19.8,0-45.4-36.9-82.3-82.3-82.3S22.6,48.7,17.6,89.4H.3Z" fill-rule="evenodd"/>
|
||||
<path d="M99,51.6c-26.4,0-47.9,21.5-47.9,48s21.5,48,48,48,2.7,0,4-.2l4.8,16.8c-2.9.4-5.8.6-8.8.6-36,0-65.2-29.2-65.2-65.2S63.1,34.4,99,34.4s1.8,0,2.7,0l-2.7,17.1ZM118.6,37.4c26.4,8.3,45.6,33,45.6,62.2s-16.4,50.2-39.8,60l-4.8-16.7c16.2-7.7,27.4-24.2,27.4-43.3s-13-38.1-31.1-44.9l2.7-17.2Z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="black" fill="currentColor">
|
||||
<path d="M354.8,69.2c12,0,21.7,3.4,28.6,10.4,7,7.2,10.6,17.5,10.6,31.5v54.8h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,5-6.8,12.2-6.8,21.3v48.5h-22.4v-51.9c0-8.4-1.8-14.7-5.5-19-3.8-4.1-8.9-6.3-15.9-6.3s-13.6,2.5-18.1,7.3c-4.5,4.8-6.8,12-6.8,21.3v48.5h-22.4v-95.6h21.3v12.2c3.6-4.3,8.1-7.5,13.4-9.8,5.4-2.3,11.3-3.4,17.9-3.4s13.6,1.3,19.2,3.9c5.5,2.9,9.8,6.8,13.1,12,3.9-5,8.9-8.9,15.2-11.8,6.3-2.7,13.1-4.1,20.6-4.1ZM466,167.2c-9.7,0-18.4-2.1-26.1-6.3-7.6-4-13.8-10.1-18.1-17.5-4.5-7.3-6.6-15.7-6.6-25.2s2.1-17.9,6.6-25.2c4.3-7.4,10.6-13.4,18.1-17.4,7.7-4.1,16.5-6.3,26.1-6.3s18.6,2.1,26.3,6.3c7.7,4.1,13.8,10,18.3,17.4,4.3,7.3,6.4,15.7,6.4,25.2s-2.1,17.9-6.4,25.2c-4.5,7.5-10.6,13.4-18.3,17.5-7.7,4.1-16.5,6.3-26.3,6.3h0ZM466,148c8.2,0,15-2.7,20.4-8.2,5.4-5.5,8.1-12.7,8.1-21.7s-2.7-16.1-8.1-21.7c-5.4-5.5-12.2-8.2-20.4-8.2s-15,2.7-20.2,8.2c-5.4,5.5-8.1,12.7-8.1,21.7s2.7,16.1,8.1,21.7c5.2,5.5,12,8.2,20.2,8.2ZM631.5,33.1v132.8h-21.5v-12.3c-3.7,4.4-8.3,7.9-13.6,10.2-5.5,2.3-11.5,3.4-18.1,3.4s-17.4-2-24.7-6.1c-7.3-4.1-13.2-9.8-17.4-17.4-4.1-7.3-6.3-15.9-6.3-25.6s2.1-18.3,6.3-25.6c4.1-7.3,10-13.1,17.4-17.2,7.3-4.1,15.6-6.1,24.7-6.1s12.2,1.1,17.4,3.2c5.2,2.1,9.8,5.4,13.4,9.7v-49h22.4ZM581.1,148c5.4,0,10.2-1.3,14.5-3.8,4.3-2.3,7.7-5.9,10.2-10.4,2.5-4.5,3.8-9.8,3.8-15.7s-1.3-11.3-3.8-15.7c-2.5-4.5-5.9-8.1-10.2-10.6-4.3-2.3-9.1-3.6-14.5-3.6s-10.2,1.3-14.5,3.6c-4.3,2.5-7.7,6.1-10.2,10.6-2.5,4.5-3.8,9.8-3.8,15.7s1.3,11.3,3.8,15.7c2.5,4.5,5.9,8.1,10.2,10.4,4.3,2.5,9.1,3.8,14.5,3.8ZM681.6,84.3c6.4-10,17.7-15,34-15v21.3c-1.7-.3-3.4-.5-5.2-.5-8.8,0-15.6,2.5-20.4,7.5-4.8,5.2-7.3,12.5-7.3,22v46.4h-22.4v-95.6h21.3v14h0ZM734.1,70.3h22.4v95.6h-22.4v-95.6ZM745.4,54.6c-4.1,0-7.5-1.3-10.2-3.9-2.7-2.4-4.2-5.9-4.1-9.5,0-3.8,1.4-7,4.1-9.7,2.7-2.5,6.1-3.8,10.2-3.8s7.5,1.3,10.2,3.6c2.7,2.5,4.1,5.5,4.1,9.3s-1.3,7.2-3.9,9.8c-2.7,2.7-6.3,4.1-10.4,4.1ZM839.5,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4v-95.6h21.3v12.3c3.8-4.5,8.4-7.7,14-10,5.5-2.3,12-3.4,19-3.4ZM964.8,160.7c-2.8,2.2-6,3.9-9.5,4.8-3.9,1.1-7.9,1.6-12,1.6-10.6,0-18.6-2.7-24.3-8.2-5.7-5.5-8.6-13.4-8.6-24v-46h-15.7v-17.9h15.7v-21.8h22.4v21.8h25.6v17.9h-25.6v45.5c0,4.7,1.1,8.2,3.4,10.6,2.3,2.5,5.5,3.8,9.8,3.8s9.1-1.3,12.5-3.9l6.3,15.9ZM1036.9,69.2c12,0,21.7,3.6,29,10.6,7.3,7,10.9,17.5,10.9,31.3v54.8h-22.4v-51.9c0-8.4-2-14.7-5.9-19-3.9-4.1-9.5-6.3-16.8-6.3s-14.7,2.5-19.5,7.3c-4.8,5-7.2,12.2-7.2,21.5v48.3h-22.4V33.1h22.4v48.3c3.8-3.9,8.2-7,13.8-9.1,5.4-2,11.5-3,18.1-3Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -2,6 +2,8 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import '@modrinth/ui/src/styles/tailwind-utilities.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'bundled-minecraft-font-mrapp';
|
||||
font-style: normal;
|
||||
@@ -83,10 +85,6 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
border-radius: var(--radius-md);
|
||||
@@ -157,4 +155,23 @@ img {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
// From the Bootstrap project
|
||||
// The MIT License (MIT)
|
||||
// Copyright (c) 2011-2023 The Bootstrap Authors
|
||||
// https://github.com/twbs/bootstrap/blob/2f617215755b066904248525a8c56ea425dde871/scss/mixins/_visually-hidden.scss#L8
|
||||
.visually-hidden {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
|
||||
&:not(caption) {
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
|
||||
@import '@modrinth/assets/omorphia.scss';
|
||||
|
||||
@@ -8,10 +8,15 @@ import {
|
||||
SearchIcon,
|
||||
StopCircleIcon,
|
||||
TrashIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
|
||||
import { formatCategoryHeader } from '@modrinth/utils'
|
||||
import {
|
||||
DropdownSelect,
|
||||
formatLoader,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -22,6 +27,8 @@ import { duplicate, remove } from '@/helpers/profile.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps({
|
||||
instances: {
|
||||
type: Array,
|
||||
@@ -121,40 +128,50 @@ const handleOptionsClick = async (args) => {
|
||||
}
|
||||
}
|
||||
|
||||
const state = useStorage(
|
||||
`${props.label}-grid-display-state`,
|
||||
{
|
||||
group: 'Group',
|
||||
sortBy: 'Name',
|
||||
},
|
||||
localStorage,
|
||||
{ mergeDefaults: true },
|
||||
)
|
||||
|
||||
const search = ref('')
|
||||
const group = ref('Group')
|
||||
const sortBy = ref('Name')
|
||||
|
||||
const filteredResults = computed(() => {
|
||||
const { group = 'Group', sortBy = 'Name' } = state.value
|
||||
|
||||
const instances = props.instances.filter((instance) => {
|
||||
return instance.name.toLowerCase().includes(search.value.toLowerCase())
|
||||
})
|
||||
|
||||
if (sortBy.value === 'Name') {
|
||||
if (sortBy === 'Name') {
|
||||
instances.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
}
|
||||
|
||||
if (sortBy.value === 'Game version') {
|
||||
if (sortBy === 'Game version') {
|
||||
instances.sort((a, b) => {
|
||||
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||
})
|
||||
}
|
||||
|
||||
if (sortBy.value === 'Last played') {
|
||||
if (sortBy === 'Last played') {
|
||||
instances.sort((a, b) => {
|
||||
return dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0))
|
||||
})
|
||||
}
|
||||
|
||||
if (sortBy.value === 'Date created') {
|
||||
if (sortBy === 'Date created') {
|
||||
instances.sort((a, b) => {
|
||||
return dayjs(b.date_created).diff(dayjs(a.date_created))
|
||||
})
|
||||
}
|
||||
|
||||
if (sortBy.value === 'Date modified') {
|
||||
if (sortBy === 'Date modified') {
|
||||
instances.sort((a, b) => {
|
||||
return dayjs(b.date_modified).diff(dayjs(a.date_modified))
|
||||
})
|
||||
@@ -162,16 +179,16 @@ const filteredResults = computed(() => {
|
||||
|
||||
const instanceMap = new Map()
|
||||
|
||||
if (group.value === 'Loader') {
|
||||
if (group === 'Loader') {
|
||||
instances.forEach((instance) => {
|
||||
const loader = formatCategoryHeader(instance.loader)
|
||||
const loader = formatLoader(formatMessage, instance.loader)
|
||||
if (!instanceMap.has(loader)) {
|
||||
instanceMap.set(loader, [])
|
||||
}
|
||||
|
||||
instanceMap.get(loader).push(instance)
|
||||
})
|
||||
} else if (group.value === 'Game version') {
|
||||
} else if (group === 'Game version') {
|
||||
instances.forEach((instance) => {
|
||||
if (!instanceMap.has(instance.game_version)) {
|
||||
instanceMap.set(instance.game_version, [])
|
||||
@@ -179,7 +196,7 @@ const filteredResults = computed(() => {
|
||||
|
||||
instanceMap.get(instance.game_version).push(instance)
|
||||
})
|
||||
} else if (group.value === 'Group') {
|
||||
} else if (group === 'Group') {
|
||||
instances.forEach((instance) => {
|
||||
if (instance.groups.length === 0) {
|
||||
instance.groups.push('None')
|
||||
@@ -199,7 +216,7 @@ const filteredResults = computed(() => {
|
||||
|
||||
// For 'name', we intuitively expect the sorting to apply to the name of the group first, not just the name of the instance
|
||||
// ie: Category A should come before B, even if the first instance in B comes before the first instance in A
|
||||
if (sortBy.value === 'Name') {
|
||||
if (sortBy === 'Name') {
|
||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||
// None should always be first
|
||||
if (a[0] === 'None' && b[0] !== 'None') {
|
||||
@@ -217,7 +234,7 @@ const filteredResults = computed(() => {
|
||||
}
|
||||
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
||||
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
||||
if (group.value === 'Game version') {
|
||||
if (group === 'Game version') {
|
||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
||||
})
|
||||
@@ -232,16 +249,17 @@ const filteredResults = computed(() => {
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex gap-2">
|
||||
<div class="iconified-input flex-1">
|
||||
<SearchIcon />
|
||||
<input v-model="search" type="text" placeholder="Search" />
|
||||
<Button class="r-btn" @click="() => (search = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
clearable
|
||||
wrapper-class="flex-1"
|
||||
/>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="sortBy"
|
||||
v-model="state.sortBy"
|
||||
name="Sort Dropdown"
|
||||
class="max-w-[16rem]"
|
||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
||||
@@ -252,7 +270,7 @@ const filteredResults = computed(() => {
|
||||
</DropdownSelect>
|
||||
<DropdownSelect
|
||||
v-slot="{ selected }"
|
||||
v-model="group"
|
||||
v-model="state.group"
|
||||
class="max-w-[16rem]"
|
||||
name="Group Dropdown"
|
||||
:options="['Group', 'Loader', 'Game version', 'None']"
|
||||
|
||||
@@ -18,8 +18,8 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import Instance from '@/components/ui/Instance.vue'
|
||||
import LegacyProjectCard from '@/components/ui/LegacyProjectCard.vue'
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_by_profile_path } from '@/helpers/process.js'
|
||||
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
||||
@@ -270,7 +270,7 @@ onUnmounted(() => {
|
||||
/>
|
||||
</section>
|
||||
<section v-else ref="modsRow" class="projects">
|
||||
<ProjectCard
|
||||
<LegacyProjectCard
|
||||
v-for="project in row.instances.slice(0, maxProjectsPerRow)"
|
||||
:key="project?.project_id"
|
||||
ref="instanceComponents"
|
||||
|
||||
@@ -284,12 +284,12 @@ onUnmounted(() => {
|
||||
z-index: 11;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
width: max-content;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
max-height: 98vh;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
@@ -380,7 +380,7 @@ onUnmounted(() => {
|
||||
text-align: left;
|
||||
|
||||
&.expanded {
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ onBeforeUnmount(() => {
|
||||
background-color: var(--color-raised-bg);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-floating);
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
z-index: 1000000;
|
||||
@@ -163,7 +163,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
margin: var(--gap-sm);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const metadata = ref({})
|
||||
|
||||
defineExpose({
|
||||
async show(errorVal, context, canClose = true, source = null) {
|
||||
console.log(errorVal, context, canClose, source)
|
||||
closable.value = canClose
|
||||
|
||||
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||
import { Button, Checkbox, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -110,31 +110,33 @@ const exportPack = async () => {
|
||||
<div class="modal-body">
|
||||
<div class="labeled_input">
|
||||
<p>Modpack Name</p>
|
||||
<div class="iconified-input">
|
||||
<PackageIcon />
|
||||
<input v-model="nameInput" type="text" placeholder="Modpack name" class="input" />
|
||||
<Button class="r-btn" @click="nameInput = ''">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
placeholder="Modpack name"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>Version number</p>
|
||||
<div class="iconified-input">
|
||||
<VersionIcon />
|
||||
<input v-model="versionInput" type="text" placeholder="1.0.0" class="input" />
|
||||
<Button class="r-btn" @click="versionInput = ''">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
placeholder="1.0.0"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<div class="labeled_input">
|
||||
<p>Description</p>
|
||||
|
||||
<div class="textarea-wrapper">
|
||||
<textarea v-model="exportDescription" placeholder="Enter modpack description..." />
|
||||
</div>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
placeholder="Enter modpack description..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -289,17 +291,4 @@ const exportPack = async () => {
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
// margin-top: 1rem;
|
||||
height: 12rem;
|
||||
|
||||
textarea {
|
||||
max-height: 12rem;
|
||||
}
|
||||
|
||||
.preview {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -69,7 +69,7 @@ const play = async (e, context) => {
|
||||
await run(props.instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
source: context,
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<p class="input-label">Name</p>
|
||||
<input
|
||||
<StyledInput
|
||||
v-model="profile_name"
|
||||
autocomplete="off"
|
||||
class="text-input"
|
||||
type="text"
|
||||
maxlength="100"
|
||||
placeholder="Enter a name for your instance..."
|
||||
:maxlength="100"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
@@ -34,7 +35,7 @@
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<p class="input-label">Game version</p>
|
||||
<div class="versions">
|
||||
<div class="flex gap-4 items-center">
|
||||
<multiselect
|
||||
v-model="game_version"
|
||||
class="selector"
|
||||
@@ -45,7 +46,7 @@
|
||||
open-direction="top"
|
||||
:show-labels="false"
|
||||
/>
|
||||
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Show all versions" />
|
||||
<Checkbox v-model="showSnapshots" class="shrink-0" label="Show all versions" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loader !== 'vanilla'" class="input-row">
|
||||
@@ -93,18 +94,14 @@
|
||||
<div class="path-selection">
|
||||
<h3>{{ selectedProfileType.name }} path</h3>
|
||||
<div class="path-input">
|
||||
<div class="iconified-input">
|
||||
<FolderOpenIcon />
|
||||
<input
|
||||
v-model="selectedProfileType.path"
|
||||
type="text"
|
||||
placeholder="Path to launcher"
|
||||
@change="setPath"
|
||||
/>
|
||||
<Button class="r-btn" @click="() => (selectedProfileType.path = '')">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-model="selectedProfileType.path"
|
||||
:icon="FolderOpenIcon"
|
||||
type="text"
|
||||
placeholder="Path to launcher"
|
||||
clearable
|
||||
@change="setPath"
|
||||
/>
|
||||
<Button icon-only @click="selectLauncherPath">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
@@ -197,7 +194,14 @@ import {
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, Checkbox, Chips, injectNotificationManager } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chips,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
@@ -341,7 +345,7 @@ const create_instance = async () => {
|
||||
creating.value = true
|
||||
const loader_version_value =
|
||||
loader_version.value === 'other' ? specified_loader_version.value : loader_version.value
|
||||
const loaderVersion = loader.value === 'vanilla' ? null : loader_version_value ?? 'stable'
|
||||
const loaderVersion = loader.value === 'vanilla' ? null : (loader_version_value ?? 'stable')
|
||||
|
||||
hide()
|
||||
creating.value = false
|
||||
@@ -350,7 +354,7 @@ const create_instance = async () => {
|
||||
profile_name.value,
|
||||
game_version.value,
|
||||
loader.value,
|
||||
loader.value === 'vanilla' ? null : loader_version_value ?? 'stable',
|
||||
loader.value === 'vanilla' ? null : (loader_version_value ?? 'stable'),
|
||||
icon.value,
|
||||
).catch(handleError)
|
||||
|
||||
@@ -546,12 +550,6 @@ const next = async () => {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.versions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
:deep(button.checkbox) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
import { Avatar, ButtonStyled, FormattedTag } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
|
||||
type Instance = {
|
||||
@@ -37,7 +36,8 @@ defineProps<{
|
||||
</span>
|
||||
<span class="text-secondary flex items-center gap-2 font-semibold">
|
||||
<GameIcon class="h-5 w-5 text-secondary" />
|
||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||
<FormattedTag :tag="instance.loader" enforce-type="loader" />
|
||||
{{ instance.game_version }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<template>
|
||||
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
||||
<div class="toggle-setting" :class="{ compact }">
|
||||
<input
|
||||
<StyledInput
|
||||
autocomplete="off"
|
||||
:disabled="props.disabled"
|
||||
:value="props.modelValue ? props.modelValue.path : ''"
|
||||
type="text"
|
||||
class="installation-input"
|
||||
:model-value="props.modelValue ? props.modelValue.path : ''"
|
||||
:placeholder="placeholder ?? '/path/to/java'"
|
||||
@input="
|
||||
wrapper-class="installation-input"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
path: val.target.value,
|
||||
path: val,
|
||||
})
|
||||
}
|
||||
"
|
||||
@@ -60,7 +59,7 @@ import {
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||
import { Button, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
|
||||
+7
-6
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
||||
import { Avatar, TagItem } from '@modrinth/ui'
|
||||
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||
import { Avatar, FormattedTag, TagItem, useCompactNumber } from '@modrinth/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed } from 'vue'
|
||||
@@ -11,6 +10,8 @@ dayjs.extend(relativeTime)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const { formatCompactNumber } = useCompactNumber()
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
@@ -63,7 +64,7 @@ const toTransparent = computed(() => {
|
||||
<div
|
||||
class="w-full aspect-[2/1] bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
'background-color': project.featured_gallery ?? project.gallery[0] ? null : toColor,
|
||||
'background-color': (project.featured_gallery ?? project.gallery[0]) ? null : toColor,
|
||||
'background-image': `url(${
|
||||
project.featured_gallery ??
|
||||
project.gallery[0] ??
|
||||
@@ -96,18 +97,18 @@ const toTransparent = computed(() => {
|
||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatNumber(project.downloads) }}
|
||||
{{ formatCompactNumber(project.downloads) }}
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-1 pr-2 border-0 border-r-[1px] border-solid border-button-border"
|
||||
>
|
||||
<HeartIcon />
|
||||
{{ formatNumber(project.follows) }}
|
||||
{{ formatCompactNumber(project.follows) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 pr-2">
|
||||
<TagIcon />
|
||||
<TagItem>
|
||||
{{ formatCategory(featuredCategory) }}
|
||||
<FormattedTag :tag="featuredCategory" />
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<nav
|
||||
v-if="filteredLinks.length > 1"
|
||||
ref="scrollContainer"
|
||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-clip rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
>
|
||||
<RouterLink
|
||||
@@ -14,13 +16,17 @@
|
||||
<span class="text-nowrap">{{ link.label }}</span>
|
||||
</RouterLink>
|
||||
<div
|
||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'}`"
|
||||
:class="[
|
||||
'pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1',
|
||||
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected',
|
||||
{ 'navtabs-transition': transitionsEnabled },
|
||||
]"
|
||||
:style="{
|
||||
left: sliderLeftPx,
|
||||
top: sliderTopPx,
|
||||
right: sliderRightPx,
|
||||
bottom: sliderBottomPx,
|
||||
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
|
||||
opacity: sliderReady && activeIndex !== -1 ? 1 : 0,
|
||||
}"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
@@ -28,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
|
||||
@@ -47,13 +53,16 @@ const props = defineProps<{
|
||||
query?: string
|
||||
}>()
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null)
|
||||
const sliderLeft = ref(4)
|
||||
const sliderTop = ref(4)
|
||||
const sliderRight = ref(4)
|
||||
const sliderBottom = ref(4)
|
||||
const activeIndex = ref(-1)
|
||||
const oldIndex = ref(-1)
|
||||
const subpageSelected = ref(false)
|
||||
const sliderReady = ref(false)
|
||||
const transitionsEnabled = ref(false)
|
||||
const sliderDelays = ref({ left: '0ms', top: '0ms', right: '0ms', bottom: '0ms' })
|
||||
|
||||
const filteredLinks = computed(() =>
|
||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
||||
@@ -63,6 +72,11 @@ const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
||||
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
||||
|
||||
const leftDelay = computed(() => sliderDelays.value.left)
|
||||
const rightDelay = computed(() => sliderDelays.value.right)
|
||||
const topDelay = computed(() => sliderDelays.value.top)
|
||||
const bottomDelay = computed(() => sliderDelays.value.bottom)
|
||||
|
||||
function pickLink() {
|
||||
let index = -1
|
||||
subpageSelected.value = false
|
||||
@@ -83,57 +97,54 @@ function pickLink() {
|
||||
if (activeIndex.value !== -1) {
|
||||
startAnimation()
|
||||
} else {
|
||||
oldIndex.value = -1
|
||||
sliderLeft.value = 0
|
||||
sliderRight.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const tabLinkElements = ref()
|
||||
function getTabElement(index: number): HTMLElement | null {
|
||||
if (index === -1) return null
|
||||
const container = scrollContainer.value
|
||||
if (!container) return null
|
||||
const tabs = container.querySelectorAll('.button-animation')
|
||||
return (tabs[index] as HTMLElement) ?? null
|
||||
}
|
||||
|
||||
function startAnimation() {
|
||||
const el = tabLinkElements.value[activeIndex.value].$el
|
||||
|
||||
if (!el || !el.offsetParent) return
|
||||
const el = getTabElement(activeIndex.value)
|
||||
if (!el?.offsetParent) return
|
||||
|
||||
const parent = el.offsetParent as HTMLElement
|
||||
const newValues = {
|
||||
left: el.offsetLeft,
|
||||
top: el.offsetTop,
|
||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||
right: parent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||
bottom: parent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||
}
|
||||
|
||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||
const isInitialPosition = sliderLeft.value === 4 && sliderRight.value === 4
|
||||
|
||||
if (isInitialPosition) {
|
||||
sliderLeft.value = newValues.left
|
||||
sliderRight.value = newValues.right
|
||||
sliderTop.value = newValues.top
|
||||
sliderBottom.value = newValues.bottom
|
||||
sliderReady.value = true
|
||||
requestAnimationFrame(() => {
|
||||
transitionsEnabled.value = true
|
||||
})
|
||||
} else {
|
||||
const delay = 200
|
||||
|
||||
if (newValues.left < sliderLeft.value) {
|
||||
sliderLeft.value = newValues.left
|
||||
setTimeout(() => {
|
||||
sliderRight.value = newValues.right
|
||||
}, delay)
|
||||
} else {
|
||||
sliderRight.value = newValues.right
|
||||
setTimeout(() => {
|
||||
sliderLeft.value = newValues.left
|
||||
}, delay)
|
||||
}
|
||||
|
||||
if (newValues.top < sliderTop.value) {
|
||||
sliderTop.value = newValues.top
|
||||
setTimeout(() => {
|
||||
sliderBottom.value = newValues.bottom
|
||||
}, delay)
|
||||
} else {
|
||||
sliderBottom.value = newValues.bottom
|
||||
setTimeout(() => {
|
||||
sliderTop.value = newValues.top
|
||||
}, delay)
|
||||
const STAGGER_DELAY = '200ms'
|
||||
sliderDelays.value = {
|
||||
left: newValues.left < sliderLeft.value ? '0ms' : STAGGER_DELAY,
|
||||
right: newValues.left < sliderLeft.value ? STAGGER_DELAY : '0ms',
|
||||
top: newValues.top < sliderTop.value ? '0ms' : STAGGER_DELAY,
|
||||
bottom: newValues.top < sliderTop.value ? STAGGER_DELAY : '0ms',
|
||||
}
|
||||
sliderLeft.value = newValues.left
|
||||
sliderRight.value = newValues.right
|
||||
sliderTop.value = newValues.top
|
||||
sliderBottom.value = newValues.bottom
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +157,17 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', pickLink)
|
||||
})
|
||||
|
||||
watch(route, () => {
|
||||
watch(
|
||||
filteredLinks,
|
||||
async () => {
|
||||
await nextTick()
|
||||
pickLink()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(route, async () => {
|
||||
await nextTick()
|
||||
pickLink()
|
||||
})
|
||||
</script>
|
||||
@@ -154,7 +175,10 @@ watch(route, () => {
|
||||
.navtabs-transition {
|
||||
/* Delay on opacity is to hide any jankiness as the page loads */
|
||||
transition:
|
||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1) 0s,
|
||||
left 150ms cubic-bezier(0.4, 0, 0.2, 1) v-bind(leftDelay),
|
||||
right 150ms cubic-bezier(0.4, 0, 0.2, 1) v-bind(rightDelay),
|
||||
top 150ms cubic-bezier(0.4, 0, 0.2, 1) v-bind(topDelay),
|
||||
bottom 150ms cubic-bezier(0.4, 0, 0.2, 1) v-bind(bottomDelay),
|
||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,18 +34,18 @@ function updateAdPosition() {
|
||||
<template>
|
||||
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
|
||||
<a
|
||||
href="https://modrinth.gg/medal?from=app-placeholder"
|
||||
href="https://modrinth.host/medal?from=app-placeholder"
|
||||
target="_blank"
|
||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
|
||||
>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/medal-modrinth-servers-light-new.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
src="https://cdn-raw.modrinth.com/modrinth-hosting-medal-light.webp"
|
||||
alt="Host your next server with Modrinth Hosting"
|
||||
class="hidden light-image rounded-[inherit]"
|
||||
/>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/medal-modrinth-servers-dark-new.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
src="https://cdn-raw.modrinth.com/modrinth-hosting-medal-dark.webp"
|
||||
alt="Host your next server with Modrinth Hosting"
|
||||
class="dark-image rounded-[inherit]"
|
||||
/>
|
||||
</a>
|
||||
|
||||
@@ -69,7 +69,10 @@ onUnmounted(() => {
|
||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||
</div>
|
||||
</NavButton>
|
||||
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-button-bg"></div>
|
||||
<div
|
||||
v-if="instances && recentInstances.length > 0"
|
||||
class="h-px w-6 mx-auto my-2 bg-divider"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -52,10 +52,12 @@
|
||||
<h3 class="info-title">
|
||||
{{ loadingBar.title }}
|
||||
</h3>
|
||||
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
||||
<div class="row">
|
||||
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}%
|
||||
{{ loadingBar.message }}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
||||
<div class="row">
|
||||
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}%
|
||||
{{ loadingBar.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -293,7 +295,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
}
|
||||
|
||||
@@ -346,7 +348,7 @@ onBeforeUnmount(() => {
|
||||
.info-card {
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
right: 0.5rem;
|
||||
right: 2rem;
|
||||
z-index: 9;
|
||||
width: 20rem;
|
||||
background-color: var(--color-raised-bg);
|
||||
@@ -356,7 +358,7 @@ onBeforeUnmount(() => {
|
||||
gap: 1rem;
|
||||
overflow: auto;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
|
||||
&.hidden {
|
||||
transform: translateY(-100%);
|
||||
@@ -420,7 +422,7 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -454,7 +456,7 @@ onBeforeUnmount(() => {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 1px solid var(--color-button-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
padding: var(--gap-md);
|
||||
|
||||
&.hidden {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="card-shadow p-4 bg-bg-raised rounded-xl flex gap-3 group cursor-pointer hover:brightness-90 transition-all"
|
||||
@click="
|
||||
<ProjectCard
|
||||
:title="project.title"
|
||||
:link="
|
||||
() => {
|
||||
emit('open')
|
||||
$router.push({
|
||||
@@ -10,116 +10,58 @@
|
||||
})
|
||||
}
|
||||
"
|
||||
:author="{ name: project.author, link: `https://modrinth.com/user/${project.author}` }"
|
||||
:icon-url="project.icon_url"
|
||||
:summary="project.description"
|
||||
:tags="project.display_categories"
|
||||
:all-tags="project.categories"
|
||||
:downloads="project.downloads"
|
||||
:followers="project.follows"
|
||||
:date-updated="project.date_modified"
|
||||
:banner="project.featured_gallery ?? undefined"
|
||||
:color="project.color ?? undefined"
|
||||
:environment="
|
||||
projectType
|
||||
? ['mod', 'modpack'].includes(projectType)
|
||||
? {
|
||||
clientSide: project.client_side,
|
||||
serverSide: project.server_side,
|
||||
}
|
||||
: undefined
|
||||
: undefined
|
||||
"
|
||||
layout="list"
|
||||
>
|
||||
<div class="icon w-[96px] h-[96px] relative">
|
||||
<Avatar :src="project.icon_url" size="96px" class="search-icon origin-top transition-all" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 overflow-hidden">
|
||||
<div class="gap-2 overflow-hidden no-wrap text-ellipsis">
|
||||
<span class="text-lg font-extrabold text-contrast m-0 leading-none">
|
||||
{{ project.title }}
|
||||
</span>
|
||||
<span v-if="project.author" class="text-secondary"> by {{ project.author }}</span>
|
||||
</div>
|
||||
<div class="m-0 line-clamp-2">
|
||||
{{ project.description }}
|
||||
</div>
|
||||
<div v-if="categories.length > 0" class="mt-auto flex items-center gap-1 no-wrap">
|
||||
<TagsIcon class="h-4 w-4 shrink-0" />
|
||||
<div
|
||||
v-if="project.project_type === 'mod' || project.project_type === 'modpack'"
|
||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<button
|
||||
:disabled="installed || installing"
|
||||
class="shrink-0 no-wrap"
|
||||
@click.stop="install()"
|
||||
>
|
||||
<template v-if="project.client_side === 'optional' && project.server_side === 'optional'">
|
||||
Client or server
|
||||
<template v-if="!installed">
|
||||
<DownloadIcon v-if="modpack || instance" />
|
||||
<PlusIcon v-else />
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(project.client_side === 'optional' || project.client_side === 'required') &&
|
||||
(project.server_side === 'optional' || project.server_side === 'unsupported')
|
||||
"
|
||||
>
|
||||
Client
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
(project.server_side === 'optional' || project.server_side === 'required') &&
|
||||
(project.client_side === 'optional' || project.client_side === 'unsupported')
|
||||
"
|
||||
>
|
||||
Server
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
project.client_side === 'unsupported' && project.server_side === 'unsupported'
|
||||
"
|
||||
>
|
||||
Unsupported
|
||||
</template>
|
||||
<template
|
||||
v-else-if="project.client_side === 'required' && project.server_side === 'required'"
|
||||
>
|
||||
Client and server
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-for="tag in categories"
|
||||
:key="tag"
|
||||
class="text-sm font-semibold text-secondary flex gap-1 px-[0.375rem] py-0.5 bg-button-bg rounded-full"
|
||||
>
|
||||
{{ formatCategory(tag.name) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 items-end shrink-0 ml-auto">
|
||||
<div class="flex items-center gap-2">
|
||||
<DownloadIcon class="shrink-0" />
|
||||
<span>
|
||||
{{ formatNumber(project.downloads) }}
|
||||
<span class="text-secondary">downloads</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<HeartIcon class="shrink-0" />
|
||||
<span>
|
||||
{{ formatNumber(project.follows ?? project.followers) }}
|
||||
<span class="text-secondary">followers</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-auto relative">
|
||||
<div class="absolute bottom-0 right-0 w-fit">
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<button
|
||||
:disabled="installed || installing"
|
||||
class="shrink-0 no-wrap"
|
||||
@click.stop="install()"
|
||||
>
|
||||
<template v-if="!installed">
|
||||
<DownloadIcon v-if="modpack || instance" />
|
||||
<PlusIcon v-else />
|
||||
</template>
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing'
|
||||
: installed
|
||||
? 'Installed'
|
||||
: modpack || instance
|
||||
? 'Install'
|
||||
: 'Add to an instance'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing'
|
||||
: installed
|
||||
? 'Installed'
|
||||
: modpack || instance
|
||||
? 'Install'
|
||||
: 'Add to an instance'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { CheckIcon, DownloadIcon, HeartIcon, PlusIcon, TagsIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||
import { CheckIcon, DownloadIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -141,10 +83,6 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
instance: {
|
||||
type: Object,
|
||||
default: null,
|
||||
@@ -157,6 +95,10 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
projectType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['open', 'install'])
|
||||
|
||||
@@ -191,7 +191,8 @@ const handleClose = async () => {
|
||||
position: absolute;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: linear-gradient(180deg, rgba(66, 131, 92, 0.275) 0%, rgba(17, 35, 43, 0.5) 97.29%),
|
||||
background:
|
||||
linear-gradient(180deg, rgba(66, 131, 92, 0.275) 0%, rgba(17, 35, 43, 0.5) 97.29%),
|
||||
linear-gradient(0deg, rgba(22, 24, 28, 0.64), rgba(22, 24, 28, 0.64));
|
||||
z-index: 9997;
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, ExternalIcon, RefreshCwIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages, ProgressBar } from '@modrinth/ui'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close' | 'restart' | 'download'): void
|
||||
}>()
|
||||
|
||||
defineProps<{
|
||||
version: string
|
||||
size: number | null
|
||||
metered: boolean
|
||||
}>()
|
||||
|
||||
const downloading = ref(false)
|
||||
const { progress } = injectAppUpdateDownloadProgress()
|
||||
|
||||
function download() {
|
||||
emit('download')
|
||||
downloading.value = true
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'app.update-toast.title',
|
||||
defaultMessage: 'Update available',
|
||||
},
|
||||
body: {
|
||||
id: 'app.update-toast.body',
|
||||
defaultMessage:
|
||||
'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.',
|
||||
},
|
||||
reload: {
|
||||
id: 'app.update-toast.reload',
|
||||
defaultMessage: 'Reload',
|
||||
},
|
||||
download: {
|
||||
id: 'app.update-toast.download',
|
||||
defaultMessage: 'Download ({size})',
|
||||
},
|
||||
downloading: {
|
||||
id: 'app.update-toast.downloading',
|
||||
defaultMessage: 'Downloading...',
|
||||
},
|
||||
changelog: {
|
||||
id: 'app.update-toast.changelog',
|
||||
defaultMessage: 'Changelog',
|
||||
},
|
||||
meteredBody: {
|
||||
id: 'app.update-toast.body.metered',
|
||||
defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`,
|
||||
},
|
||||
downloadCompleteTitle: {
|
||||
id: 'app.update-toast.title.download-complete',
|
||||
defaultMessage: 'Download complete',
|
||||
},
|
||||
downloadedBody: {
|
||||
id: 'app.update-toast.body.download-complete',
|
||||
defaultMessage: `Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App.`,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-[min-content] fixed card-shadow rounded-2xl top-[--top-bar-height] mt-6 right-6 p-4 z-10 bg-bg-raised border-divider border-solid border-[2px]"
|
||||
:class="{
|
||||
'download-complete': progress === 1,
|
||||
}"
|
||||
>
|
||||
<div class="flex min-w-[25rem] gap-4">
|
||||
<h2 class="whitespace-nowrap text-base text-contrast font-semibold m-0 grow">
|
||||
{{
|
||||
formatMessage(metered && progress === 1 ? messages.downloadCompleteTitle : messages.title)
|
||||
}}
|
||||
</h2>
|
||||
<ButtonStyled size="small" circular>
|
||||
<button v-tooltip="formatMessage(commonMessages.closeButton)" @click="emit('close')">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<p class="text-sm mt-2 mb-0">
|
||||
{{
|
||||
formatMessage(
|
||||
metered
|
||||
? progress === 1
|
||||
? messages.downloadedBody
|
||||
: messages.meteredBody
|
||||
: messages.body,
|
||||
{ version },
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p
|
||||
v-if="metered && progress < 1"
|
||||
class="text-sm text-secondary mt-2 mb-0 flex items-center gap-1"
|
||||
>
|
||||
<template v-if="progress > 0">
|
||||
<ProgressBar :progress="progress" class="max-w-[unset]" />
|
||||
</template>
|
||||
</p>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button v-if="metered && progress < 1" :disabled="downloading" @click="download">
|
||||
<SpinnerIcon v-if="downloading" class="animate-spin" />
|
||||
<DownloadIcon v-else />
|
||||
{{
|
||||
formatMessage(downloading ? messages.downloading : messages.download, {
|
||||
size: formatBytes(size ?? 0),
|
||||
})
|
||||
}}
|
||||
</button>
|
||||
<button v-else @click="emit('restart')">
|
||||
<RefreshCwIcon /> {{ formatMessage(messages.reload) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a href="https://modrinth.com/news/changelog?filter=app">
|
||||
{{ formatMessage(messages.changelog) }} <ExternalIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,12 +3,13 @@ import { MailIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/ass
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { IntlFormatted } from '@vintl/vintl/components'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import FriendsSection from '@/components/ui/friends/FriendsSection.vue'
|
||||
@@ -270,15 +271,14 @@ const messages = defineMessages({
|
||||
{{ formatMessage(messages.usernameDescription) }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-4">
|
||||
<div class="iconified-input flex-1">
|
||||
<UserIcon aria-hidden="true" />
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||
@keyup.enter="addFriendFromModal"
|
||||
/>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-model="username"
|
||||
:icon="UserIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||
wrapper-class="flex-1"
|
||||
@keyup.enter="addFriendFromModal"
|
||||
/>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||
<SendIcon />
|
||||
@@ -299,23 +299,15 @@ const messages = defineMessages({
|
||||
<UserPlusIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="iconified-input flex-1">
|
||||
<input
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="friends-search-bar flex w-full"
|
||||
:placeholder="formatMessage(messages.searchFriends)"
|
||||
@keyup.esc="search = ''"
|
||||
/>
|
||||
<button
|
||||
v-if="search"
|
||||
v-tooltip="formatMessage(commonMessages.clearButton)"
|
||||
class="r-btn flex items-center justify-center bg-transparent button-animation p-2 cursor-pointer appearance-none border-none"
|
||||
@click="search = ''"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.searchFriends)"
|
||||
clearable
|
||||
variant="outlined"
|
||||
wrapper-class="flex-1"
|
||||
@keyup.esc="search = ''"
|
||||
/>
|
||||
</template>
|
||||
<h3 v-else class="ml-2 w-full text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.friends) }}
|
||||
@@ -412,16 +404,3 @@ const messages = defineMessages({
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.friends-search-bar {
|
||||
background: none;
|
||||
border: 2px solid var(--color-button-bg) !important;
|
||||
padding: 8px;
|
||||
border-radius: 12px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.friends-search-bar::placeholder {
|
||||
@apply text-sm font-normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { MoreVerticalIcon, TrashIcon, UserIcon, XIcon } from '@modrinth/assets'
|
||||
import { Accordion, Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
OverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup>
|
||||
import { CheckIcon, PlusIcon, SearchIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
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'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const modal = ref()
|
||||
const searchFilter = ref('')
|
||||
const profiles = ref([])
|
||||
|
||||
const serverName = ref('')
|
||||
const serverAddress = ref('')
|
||||
|
||||
const shownProfiles = computed(() =>
|
||||
profiles.value.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
}),
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
show: async (name, address) => {
|
||||
serverName.value = name
|
||||
serverAddress.value = address
|
||||
searchFilter.value = ''
|
||||
|
||||
const profilesVal = await list().catch(handleError)
|
||||
for (const profile of profilesVal) {
|
||||
profile.adding = false
|
||||
profile.added = false
|
||||
|
||||
try {
|
||||
const worlds = await get_profile_worlds(profile.path)
|
||||
profile.added = worlds.some((w) => w.type === 'server' && w.address === serverAddress.value)
|
||||
} catch {
|
||||
// Ignore - will show as not added
|
||||
}
|
||||
}
|
||||
|
||||
profiles.value = profilesVal
|
||||
modal.value.show()
|
||||
|
||||
trackEvent('AddServerToInstanceStart', { source: 'AddServerToInstanceModal' })
|
||||
},
|
||||
})
|
||||
|
||||
async function addServer(profile) {
|
||||
profile.adding = true
|
||||
try {
|
||||
await add_server_to_profile(profile.path, serverName.value, serverAddress.value, 'prompt')
|
||||
profile.added = true
|
||||
|
||||
trackEvent('AddServerToInstance', {
|
||||
server_name: serverName.value,
|
||||
instance_name: profile.name,
|
||||
source: 'AddServerToInstanceModal',
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
profile.adding = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="modal" header="Add server to instance">
|
||||
<div class="flex flex-col gap-4 min-w-[350px]">
|
||||
<Admonition type="warning" body="This server may not be compatible with all instances." />
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
:icon="SearchIcon"
|
||||
type="search"
|
||||
placeholder="Search for an instance"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="max-h-[21rem] overflow-y-auto">
|
||||
<div
|
||||
v-for="profile in shownProfiles"
|
||||
:key="profile.path"
|
||||
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)}`"
|
||||
@click="modal.hide()"
|
||||
>
|
||||
<Avatar
|
||||
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
||||
class="mr-2 [--size:2rem]"
|
||||
/>
|
||||
{{ profile.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>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="modal.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
@@ -28,7 +28,7 @@
|
||||
:custom-label="
|
||||
(version) =>
|
||||
`${version?.name} (${version?.loaders
|
||||
.map((name) => formatCategory(name))
|
||||
.map((name) => formatLoader(formatMessage, name))
|
||||
.join(', ')} - ${version?.game_versions.join(', ')})`
|
||||
"
|
||||
:max-height="150"
|
||||
@@ -36,7 +36,9 @@
|
||||
<span v-else>
|
||||
<span>
|
||||
{{ selectedVersion?.name }} ({{
|
||||
selectedVersion?.loaders.map((name) => formatCategory(name)).join(', ')
|
||||
selectedVersion?.loaders
|
||||
.map((name) => formatLoader(formatMessage, name))
|
||||
.join(', ')
|
||||
}}
|
||||
- {{ selectedVersion?.game_versions.join(', ') }})
|
||||
</span>
|
||||
@@ -57,8 +59,7 @@
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
import { Button, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
|
||||
@@ -67,6 +68,7 @@ import { trackEvent } from '@/helpers/analytics'
|
||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const instance = ref(null)
|
||||
const project = ref(null)
|
||||
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
DownloadIcon,
|
||||
PlusIcon,
|
||||
RightArrowIcon,
|
||||
SearchIcon,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||
import { Avatar, Button, Card, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -15,6 +16,7 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_v3_many } from '@/helpers/cache.js'
|
||||
import {
|
||||
add_project_from_version as installMod,
|
||||
check_installed,
|
||||
@@ -48,19 +50,14 @@ const creatingInstance = ref(false)
|
||||
const profiles = ref([])
|
||||
|
||||
const shownProfiles = computed(() =>
|
||||
profiles.value
|
||||
.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
})
|
||||
.filter((profile) => {
|
||||
const version = {
|
||||
game_versions: versions.value.flatMap((v) => v.game_versions),
|
||||
loaders: versions.value.flatMap((v) => v.loaders),
|
||||
}
|
||||
return isVersionCompatible(version, project.value, profile)
|
||||
}),
|
||||
profiles.value.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
}),
|
||||
)
|
||||
|
||||
const isProfileCompatible = (profile) =>
|
||||
versions.value?.some((version) => isVersionCompatible(version, project.value, profile))
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
@@ -85,6 +82,22 @@ defineExpose({
|
||||
handleError,
|
||||
)
|
||||
}
|
||||
|
||||
const linkedProjectIds = profilesVal
|
||||
.filter((p) => p.linked_data?.project_id)
|
||||
.map((p) => p.linked_data.project_id)
|
||||
if (linkedProjectIds.length > 0) {
|
||||
const linkedProjects = await get_project_v3_many(linkedProjectIds, 'must_revalidate').catch(
|
||||
() => [],
|
||||
)
|
||||
const serverProjectIds = new Set(
|
||||
linkedProjects.filter((p) => p?.minecraft_server != null).map((p) => p.id),
|
||||
)
|
||||
for (const profile of profilesVal) {
|
||||
profile.isServerInstance = serverProjectIds.has(profile.linked_data?.project_id)
|
||||
}
|
||||
}
|
||||
|
||||
profiles.value = profilesVal
|
||||
|
||||
installModal.value.show()
|
||||
@@ -159,20 +172,21 @@ const reset_icon = () => {
|
||||
const createInstance = async () => {
|
||||
creatingInstance.value = true
|
||||
|
||||
const loader =
|
||||
versions.value[0].loaders[0] !== 'forge' &&
|
||||
versions.value[0].loaders[0] !== 'fabric' &&
|
||||
versions.value[0].loaders[0] !== 'quilt'
|
||||
? 'vanilla'
|
||||
: versions.value[0].loaders[0]
|
||||
const gameVersions = versions.value[0].game_versions
|
||||
const gameVersion = gameVersions[0]
|
||||
|
||||
const id = await create(
|
||||
name.value,
|
||||
versions.value[0].game_versions[0],
|
||||
loader,
|
||||
'latest',
|
||||
icon.value,
|
||||
).catch(handleError)
|
||||
const loaders = versions.value[0].loaders
|
||||
const loader = loaders.includes('fabric')
|
||||
? 'fabric'
|
||||
: loaders.includes('neoforge')
|
||||
? 'neoforge'
|
||||
: loaders.includes('forge')
|
||||
? 'forge'
|
||||
: loaders.includes('quilt')
|
||||
? 'quilt'
|
||||
: 'vanilla'
|
||||
|
||||
const id = await create(name.value, gameVersion, loader, 'latest', icon.value).catch(handleError)
|
||||
|
||||
await installMod(id, versions.value[0].id).catch(handleError)
|
||||
|
||||
@@ -210,12 +224,12 @@ const createInstance = async () => {
|
||||
<template>
|
||||
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
||||
<div class="modal-body">
|
||||
<input
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
class="search"
|
||||
:icon="SearchIcon"
|
||||
type="search"
|
||||
placeholder="Search for an instance"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
|
||||
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
|
||||
@@ -238,17 +252,23 @@ const createInstance = async () => {
|
||||
"
|
||||
>
|
||||
<Button
|
||||
:disabled="profile.installedMod || profile.installing"
|
||||
:disabled="
|
||||
!isProfileCompatible(profile) || profile.installedMod || profile.installing
|
||||
"
|
||||
@click="install(profile)"
|
||||
>
|
||||
<DownloadIcon v-if="!profile.installedMod && !profile.installing" />
|
||||
<DownloadIcon
|
||||
v-if="isProfileCompatible(profile) && !profile.installedMod && !profile.installing"
|
||||
/>
|
||||
<CheckIcon v-else-if="profile.installedMod" />
|
||||
{{
|
||||
profile.installing
|
||||
? 'Installing...'
|
||||
: profile.installedMod
|
||||
? 'Installed'
|
||||
: 'Install'
|
||||
: !isProfileCompatible(profile)
|
||||
? 'Incompatible'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -270,7 +290,7 @@ const createInstance = async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="creation-settings">
|
||||
<input
|
||||
<StyledInput
|
||||
v-model="name"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { computed, type Ref, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -244,13 +246,12 @@ const messages = defineMessages({
|
||||
{{ formatMessage(messages.name) }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<input
|
||||
<StyledInput
|
||||
id="instance-name"
|
||||
v-model="title"
|
||||
autocomplete="off"
|
||||
maxlength="80"
|
||||
class="flex-grow"
|
||||
type="text"
|
||||
:maxlength="80"
|
||||
wrapper-class="flex-grow"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="instance.install_stage == 'installed'">
|
||||
@@ -291,9 +292,8 @@ const messages = defineMessages({
|
||||
@click="toggleGroup(group)"
|
||||
/>
|
||||
<div class="flex gap-2 items-center">
|
||||
<input
|
||||
<StyledInput
|
||||
v-model="newCategoryInput"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||
@submit="() => addCategory"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import {
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { edit } from '@/helpers/profile'
|
||||
@@ -109,14 +114,13 @@ const messages = defineMessages({
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.preLaunchDescription) }}
|
||||
</p>
|
||||
<input
|
||||
<StyledInput
|
||||
id="pre-launch"
|
||||
v-model="hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||
class="w-full mt-2"
|
||||
wrapper-class="w-full mt-2"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||
@@ -125,14 +129,13 @@ const messages = defineMessages({
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.wrapperDescription) }}
|
||||
</p>
|
||||
<input
|
||||
<StyledInput
|
||||
id="wrapper"
|
||||
v-model="hooks.wrapper"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||
class="w-full mt-2"
|
||||
wrapper-class="w-full mt-2"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||
@@ -141,14 +144,13 @@ const messages = defineMessages({
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.postExitDescription) }}
|
||||
</p>
|
||||
<input
|
||||
<StyledInput
|
||||
id="post-exit"
|
||||
v-model="hooks.post_exit"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.postExitEnter)"
|
||||
class="w-full mt-2"
|
||||
wrapper-class="w-full mt-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DownloadIcon,
|
||||
getLoaderIcon,
|
||||
HammerIcon,
|
||||
IssuesIcon,
|
||||
SpinnerIcon,
|
||||
@@ -15,24 +16,20 @@ import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Chips,
|
||||
Combobox,
|
||||
defineMessages,
|
||||
formatLoader,
|
||||
injectNotificationManager,
|
||||
TeleportDropdownMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
formatCategory,
|
||||
type GameVersionTag,
|
||||
type PlatformTag,
|
||||
type Project,
|
||||
type Version,
|
||||
} from '@modrinth/utils'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import type { GameVersionTag, PlatformTag, Project, Version } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project, get_version_many } from '@/helpers/cache'
|
||||
import { get_project, get_version, get_version_many } from '@/helpers/cache'
|
||||
import { get_loader_versions } from '@/helpers/metadata'
|
||||
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
@@ -52,6 +49,9 @@ const modalConfirmUnpair = ref()
|
||||
const modalConfirmReinstall = ref()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
const emit = defineEmits<{
|
||||
unlinked: []
|
||||
}>()
|
||||
|
||||
const loader = ref(props.instance.loader)
|
||||
const gameVersion = ref(props.instance.game_version)
|
||||
@@ -113,6 +113,12 @@ if (props.instance.linked_data && props.instance.linked_data.project_id && !prop
|
||||
versions.find(
|
||||
(version: Version) => version.id === props.instance.linked_data?.version_id,
|
||||
) ?? null
|
||||
|
||||
if (!modpackVersion.value) {
|
||||
get_version(props.instance.linked_data?.version_id, 'bypass')
|
||||
.then((version: Version) => (modpackVersion.value = version ?? null))
|
||||
.catch(handleError)
|
||||
}
|
||||
})
|
||||
.catch(handleError)
|
||||
.finally(() => {
|
||||
@@ -128,9 +134,7 @@ if (props.instance.linked_data && props.instance.linked_data.project_id && !prop
|
||||
fetching.value = false
|
||||
}
|
||||
|
||||
const currentLoaderIcon = computed(
|
||||
() => loaders?.value.find((x) => x.name === props.instance.loader)?.icon,
|
||||
)
|
||||
const currentLoaderIcon = computed(() => getLoaderIcon(props.instance.loader))
|
||||
|
||||
const gameVersionsForLoader = computed(() => {
|
||||
return all_game_versions?.value.filter((item) => {
|
||||
@@ -158,6 +162,21 @@ const selectableGameVersionNumbers = computed(() => {
|
||||
.map((x) => x.version)
|
||||
})
|
||||
|
||||
const gameVersionOptions = computed(() =>
|
||||
(selectableGameVersionNumbers.value ?? []).map((v) => ({ value: v, label: v })),
|
||||
)
|
||||
|
||||
const loaderVersionOptions = computed(() =>
|
||||
(selectableLoaderVersions.value ?? []).map((opt, index) => ({ value: index, label: opt.id })),
|
||||
)
|
||||
|
||||
const loaderVersionLabel = computed(() => {
|
||||
const idx = loaderVersionIndex.value
|
||||
return idx >= 0 && selectableLoaderVersions.value
|
||||
? selectableLoaderVersions.value[idx]?.id
|
||||
: 'Select version'
|
||||
})
|
||||
|
||||
const selectableLoaderVersions: ComputedRef<ManifestLoaderVersion[] | undefined> = computed(() => {
|
||||
if (gameVersion.value) {
|
||||
if (loader.value === 'fabric') {
|
||||
@@ -257,7 +276,7 @@ async function unpairProfile() {
|
||||
modpackProject.value = null
|
||||
modpackVersion.value = null
|
||||
modpackVersions.value = null
|
||||
modalConfirmUnpair.value.hide()
|
||||
emit('unlinked')
|
||||
}
|
||||
|
||||
async function repairModpack() {
|
||||
@@ -414,6 +433,18 @@ const messages = defineMessages({
|
||||
id: 'instance.settings.tabs.installation.unlink.description',
|
||||
defaultMessage: `This instance is linked to a modpack, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the modpack.`,
|
||||
},
|
||||
unlinkServerTitle: {
|
||||
id: 'instance.settings.tabs.installation.unlink-server.title',
|
||||
defaultMessage: 'Unlink from server',
|
||||
},
|
||||
unlinkServerDescription: {
|
||||
id: 'instance.settings.tabs.installation.unlink-server.description',
|
||||
defaultMessage: `This instance is linked to a server, which means mods can't be updated and you can't change the mod loader or Minecraft version. Unlinking will permanently disconnect this instance from the server.`,
|
||||
},
|
||||
unlinkServerVanillaDescription: {
|
||||
id: 'instance.settings.tabs.installation.unlink-server-vanilla.description',
|
||||
defaultMessage: `This instance is linked to a server, which means you can't change the Minecraft version. Unlinking will permanently disconnect this instance from the server.`,
|
||||
},
|
||||
unlinkInstanceButton: {
|
||||
id: 'instance.settings.tabs.installation.unlink.button',
|
||||
defaultMessage: 'Unlink instance',
|
||||
@@ -425,7 +456,7 @@ const messages = defineMessages({
|
||||
unlinkInstanceConfirmDescription: {
|
||||
id: 'instance.settings.tabs.installation.unlink.confirm.description',
|
||||
defaultMessage:
|
||||
'If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal.',
|
||||
'If you proceed, you will not be able to re-link it without creating an entirely new instance. You will no longer receive modpack updates and it will become a normal instance.',
|
||||
},
|
||||
reinstallModpackConfirmTitle: {
|
||||
id: 'instance.settings.tabs.installation.reinstall.confirm.title',
|
||||
@@ -534,7 +565,7 @@ const messages = defineMessages({
|
||||
v-else
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 [&_svg]:h-full [&_svg]:w-full"
|
||||
>
|
||||
<div v-if="!!currentLoaderIcon" class="contents" v-html="currentLoaderIcon" />
|
||||
<component :is="currentLoaderIcon" v-if="currentLoaderIcon" />
|
||||
<WrenchIcon v-else />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-center">
|
||||
@@ -547,17 +578,27 @@ const messages = defineMessages({
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="text-sm text-secondary leading-none">
|
||||
<span class="text-sm text-secondary leading-none capitalize">
|
||||
{{
|
||||
modpackProject
|
||||
? modpackVersion
|
||||
? modpackVersion?.version_number
|
||||
: 'Unknown version'
|
||||
: formatCategory(instance.loader)
|
||||
: props.isMinecraftServer
|
||||
? ''
|
||||
: 'Unknown version'
|
||||
: formatLoader(formatMessage, instance.loader)
|
||||
}}
|
||||
<template v-if="instance.loader !== 'vanilla' && !modpackProject">
|
||||
{{ instance.loader_version || formatMessage(messages.unknownVersion) }}
|
||||
</template>
|
||||
<template
|
||||
v-else-if="
|
||||
instance.loader && instance.loader !== 'vanilla' && props.isMinecraftServer
|
||||
"
|
||||
>
|
||||
{{ instance.loader }}
|
||||
{{ instance.loader_version }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -589,7 +630,10 @@ const messages = defineMessages({
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="modpackProject" hover-color-fill="background">
|
||||
<ButtonStyled
|
||||
v-if="modpackProject && !props.isMinecraftServer"
|
||||
hover-color-fill="background"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
changingVersion
|
||||
@@ -647,11 +691,11 @@ const messages = defineMessages({
|
||||
{{ formatMessage(messages.gameVersion) }}
|
||||
</h2>
|
||||
<div class="flex flex-wrap mt-2 gap-2">
|
||||
<TeleportDropdownMenu
|
||||
<Combobox
|
||||
v-if="selectableGameVersionNumbers !== undefined"
|
||||
v-model="gameVersion"
|
||||
:options="selectableGameVersionNumbers"
|
||||
name="Game Version Dropdown"
|
||||
:options="gameVersionOptions"
|
||||
:display-value="gameVersion || formatMessage(messages.unknownVersion)"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="hasSnapshots"
|
||||
@@ -661,16 +705,19 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<template v-if="loader !== 'vanilla'">
|
||||
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.loaderVersion, { loader: formatCategory(loader) }) }}
|
||||
{{
|
||||
formatMessage(messages.loaderVersion, {
|
||||
loader: formatLoader(formatMessage, loader),
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
<TeleportDropdownMenu
|
||||
<Combobox
|
||||
v-if="selectableLoaderVersions"
|
||||
:model-value="selectableLoaderVersions[loaderVersionIndex]"
|
||||
:options="selectableLoaderVersions"
|
||||
:display-name="(option: ManifestLoaderVersion) => option?.id"
|
||||
v-model="loaderVersionIndex"
|
||||
:options="loaderVersionOptions"
|
||||
:display-value="loaderVersionLabel"
|
||||
name="Version selector"
|
||||
class="mt-2"
|
||||
@change="(value) => (loaderVersionIndex = value.index)"
|
||||
/>
|
||||
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
|
||||
<IssuesIcon />
|
||||
@@ -694,7 +741,7 @@ const messages = defineMessages({
|
||||
? messages.alreadyInstalledVanilla
|
||||
: messages.alreadyInstalledModded,
|
||||
{
|
||||
platform: formatCategory(loader),
|
||||
platform: formatLoader(formatMessage, loader),
|
||||
version: instance.loader_version,
|
||||
game_version: gameVersion,
|
||||
},
|
||||
@@ -741,17 +788,29 @@ const messages = defineMessages({
|
||||
<template v-else>
|
||||
<template v-if="instance.linked_data && instance.linked_data.locked">
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.unlinkInstanceTitle) }}
|
||||
{{
|
||||
formatMessage(
|
||||
props.isMinecraftServer ? messages.unlinkServerTitle : messages.unlinkInstanceTitle,
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.unlinkInstanceDescription) }}
|
||||
{{
|
||||
formatMessage(
|
||||
props.isMinecraftServer
|
||||
? instance.loader === 'vanilla'
|
||||
? messages.unlinkServerVanillaDescription
|
||||
: messages.unlinkServerDescription
|
||||
: messages.unlinkInstanceDescription,
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<ButtonStyled>
|
||||
<button class="mt-2" @click="modalConfirmUnpair.show()">
|
||||
<UnlinkIcon /> {{ formatMessage(messages.unlinkInstanceButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template v-if="modpackProject">
|
||||
<template v-if="modpackProject && !props.isMinecraftServer">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast block mt-4">
|
||||
{{ formatMessage(messages.reinstallModpackTitle) }}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||
import { Checkbox, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import {
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
Slider,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, readonly, ref, watch } from 'vue'
|
||||
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
@@ -9,7 +15,7 @@ import useMemorySlider from '@/composables/useMemorySlider'
|
||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
|
||||
import type { AppSettings, InstanceSettingsTabProps, MemorySettings } from '../../../helpers/types'
|
||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -22,12 +28,12 @@ const overrideJavaInstall = ref(!!props.instance.java_path)
|
||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
||||
const javaInstall = ref({ path: optimalJava.path ?? props.instance.java_path })
|
||||
|
||||
const overrideJavaArgs = ref(props.instance.extra_launch_args?.length !== undefined)
|
||||
const overrideJavaArgs = ref((props.instance.extra_launch_args?.length ?? 0) > 0)
|
||||
const javaArgs = ref(
|
||||
(props.instance.extra_launch_args ?? globalSettings.extra_launch_args).join(' '),
|
||||
)
|
||||
|
||||
const overrideEnvVars = ref(props.instance.custom_env_vars?.length !== undefined)
|
||||
const overrideEnvVars = ref((props.instance.custom_env_vars?.length ?? 0) > 0)
|
||||
const envVars = ref(
|
||||
(props.instance.custom_env_vars ?? globalSettings.custom_env_vars)
|
||||
.map((x) => x.join('='))
|
||||
@@ -42,36 +48,23 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
|
||||
}
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
java_path?: string
|
||||
extra_launch_args?: string[]
|
||||
custom_env_vars?: string[][]
|
||||
memory?: MemorySettings
|
||||
} = {}
|
||||
|
||||
if (overrideJavaInstall.value) {
|
||||
if (javaInstall.value.path !== '') {
|
||||
editProfile.java_path = javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
||||
}
|
||||
return {
|
||||
java_path:
|
||||
overrideJavaInstall.value && javaInstall.value.path !== ''
|
||||
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
||||
: null,
|
||||
extra_launch_args: overrideJavaArgs.value
|
||||
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
: null,
|
||||
custom_env_vars: overrideEnvVars.value
|
||||
? envVars.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
: null,
|
||||
memory: overrideMemorySettings.value ? memory.value : null,
|
||||
}
|
||||
|
||||
if (overrideJavaArgs.value) {
|
||||
editProfile.extra_launch_args = javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
}
|
||||
|
||||
if (overrideEnvVars.value) {
|
||||
editProfile.custom_env_vars = envVars.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
}
|
||||
|
||||
if (overrideMemorySettings.value) {
|
||||
editProfile.memory = memory.value
|
||||
}
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -169,27 +162,25 @@ const messages = defineMessages({
|
||||
{{ formatMessage(messages.javaArguments) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
||||
<input
|
||||
<StyledInput
|
||||
id="java-args"
|
||||
v-model="javaArgs"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideJavaArgs"
|
||||
type="text"
|
||||
class="w-full"
|
||||
placeholder="Enter java arguments..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
||||
<input
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="envVars"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideEnvVars"
|
||||
type="text"
|
||||
class="w-full"
|
||||
placeholder="Enter environmental variables..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { Checkbox, injectNotificationManager, Toggle } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import {
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
Toggle,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, type Ref, ref, watch } from 'vue'
|
||||
|
||||
import { edit } from '@/helpers/profile'
|
||||
@@ -26,20 +32,16 @@ const fullscreenSetting: Ref<boolean> = ref(
|
||||
)
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
force_fullscreen?: boolean
|
||||
game_resolution?: [number, number]
|
||||
} = {}
|
||||
|
||||
if (overrideWindowSettings.value) {
|
||||
editProfile.force_fullscreen = fullscreenSetting.value
|
||||
|
||||
if (!fullscreenSetting.value) {
|
||||
editProfile.game_resolution = resolution.value
|
||||
if (!overrideWindowSettings.value) {
|
||||
return {
|
||||
force_fullscreen: null,
|
||||
game_resolution: null,
|
||||
}
|
||||
}
|
||||
|
||||
return editProfile
|
||||
return {
|
||||
force_fullscreen: fullscreenSetting.value,
|
||||
game_resolution: fullscreenSetting.value ? null : resolution.value,
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -95,14 +97,6 @@ const messages = defineMessages({
|
||||
<Checkbox
|
||||
v-model="overrideWindowSettings"
|
||||
:label="formatMessage(messages.customWindowSettings)"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
if (!value) {
|
||||
resolution = globalSettings.game_resolution
|
||||
fullscreenSetting = globalSettings.force_fullscreen
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="mt-2 flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
@@ -134,7 +128,7 @@ const messages = defineMessages({
|
||||
{{ formatMessage(messages.widthDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
<StyledInput
|
||||
id="width"
|
||||
v-model="resolution[0]"
|
||||
autocomplete="off"
|
||||
@@ -153,7 +147,7 @@ const messages = defineMessages({
|
||||
{{ formatMessage(messages.heightDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
<StyledInput
|
||||
id="height"
|
||||
v-model="resolution[1]"
|
||||
autocomplete="off"
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
DropdownIcon,
|
||||
LogInIcon,
|
||||
MessagesSquareIcon,
|
||||
WrenchIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, Collapsible, NewModal } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
import { type MinecraftAuthError, minecraftAuthErrors } from './minecraft-auth-errors'
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const rawError = ref<string>('')
|
||||
const matchedError = ref<MinecraftAuthError | null>(null)
|
||||
const debugCollapsed = ref(true)
|
||||
const copied = ref(false)
|
||||
const loadingSignIn = ref(false)
|
||||
|
||||
function show(errorVal: { message?: string }) {
|
||||
rawError.value = errorVal?.message ?? String(errorVal)
|
||||
|
||||
matchedError.value = minecraftAuthErrors.find((e) => rawError.value.includes(e.errorCode)) ?? null
|
||||
|
||||
debugCollapsed.value = true
|
||||
hide_ads_window()
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
onModalHide()
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
function onModalHide() {
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
|
||||
async function signInAgain() {
|
||||
try {
|
||||
loadingSignIn.value = true
|
||||
const loggedIn = await login_flow()
|
||||
if (loggedIn) {
|
||||
await set_default_user(loggedIn.profile.id)
|
||||
}
|
||||
loadingSignIn.value = false
|
||||
modal.value?.hide()
|
||||
} catch (err) {
|
||||
loadingSignIn.value = false
|
||||
handleSevereError(err)
|
||||
}
|
||||
}
|
||||
|
||||
const debugInfo = computed(() => rawError.value || 'No error message.')
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NewModal ref="modal" header="Sign in Failed" :max-width="'548px'" @hide="onModalHide">
|
||||
<div class="flex flex-col gap-6">
|
||||
<Admonition
|
||||
type="warning"
|
||||
body=" We couldn't sign you into your Microsoft account. This may be due to account restrictions or
|
||||
regional limitations."
|
||||
>
|
||||
</Admonition>
|
||||
|
||||
<!-- Matched error details -->
|
||||
<div class="bg-surface-2 rounded-2xl p-4 px-5 flex flex-col gap-3">
|
||||
<template v-if="matchedError">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h3 class="text-base font-bold m-0">What we think happened</h3>
|
||||
<p class="text-sm text-secondary m-0">
|
||||
{{ matchedError.whatHappened }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h3 class="text-base font-bold m-0">How to fix it</h3>
|
||||
<ol class="list-none flex flex-col gap-2 m-0 pl-0">
|
||||
<li
|
||||
v-for="(step, index) in matchedError.stepsToFix"
|
||||
:key="index"
|
||||
class="flex items-baseline gap-2"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center shrink-0 w-5 h-5 rounded-full bg-surface-4 border border-solid border-surface-5 text-xs font-medium"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span
|
||||
class="text-sm [&_a]:text-info [&_a]:font-medium [&_a]:underline"
|
||||
v-html="step"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<h3 class="text-base font-bold m-0">Unknown error</h3>
|
||||
<p class="text-sm text-secondary m-0">
|
||||
We don’t recognize this error and can’t recommend specific steps to resolve it.
|
||||
</p>
|
||||
<p class="text-sm text-secondary m-0">
|
||||
Try visiting
|
||||
<a
|
||||
class="text-info font-medium underline hover:underline"
|
||||
href="https://www.minecraft.net/en-us/login"
|
||||
>Minecraft Login</a
|
||||
>
|
||||
and signing in, as it may prompt you with the necessary steps. You can also contact
|
||||
support and we can look into it further.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<a href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||
<MessagesSquareIcon /> Contact support
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="loadingSignIn" class="!w-full" @click="signInAgain">
|
||||
<LogInIcon /> Sign in again
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="w-full h-[1px] bg-surface-5"></div>
|
||||
|
||||
<!-- Debug info -->
|
||||
<div class="overflow-clip">
|
||||
<button
|
||||
class="flex items-center justify-between w-full bg-transparent border-0 py-4 cursor-pointer"
|
||||
@click="debugCollapsed = !debugCollapsed"
|
||||
>
|
||||
<span class="flex items-center gap-2 text-contrast font-extrabold m-0">
|
||||
<WrenchIcon class="h-4 w-4" />
|
||||
Debug information
|
||||
</span>
|
||||
<DropdownIcon
|
||||
class="h-5 w-5 text-secondary transition-transform"
|
||||
:class="{ 'rotate-180': !debugCollapsed }"
|
||||
/>
|
||||
</button>
|
||||
<Collapsible :collapsed="debugCollapsed">
|
||||
<div
|
||||
class="p-3 bg-surface-2 rounded-2xl text-xs grid grid-cols-[1fr_auto] max-w-full items-start"
|
||||
>
|
||||
<div
|
||||
class="m-0 p-0 rounded-none bg-transparent text-sm font-mono break-words overflow-auto"
|
||||
>
|
||||
{{ debugInfo }}
|
||||
</div>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
export interface MinecraftAuthError {
|
||||
errorCode: string
|
||||
whatHappened: string
|
||||
stepsToFix: string[]
|
||||
}
|
||||
|
||||
export const minecraftAuthErrors: MinecraftAuthError[] = [
|
||||
{
|
||||
errorCode: '2148916222',
|
||||
whatHappened:
|
||||
'Your Minecraft/Xbox Live account requires age verification to comply with UK regulations. You must complete this before signing in.',
|
||||
stepsToFix: [
|
||||
'Go to the <a href="https://www.minecraft.net/en-us/login">Minecraft Login</a> page and sign in',
|
||||
'Follow the instructions to verify your age',
|
||||
'Once verified, try signing in again',
|
||||
'For additional help, visit <a href="https://support.xbox.com/en-GB/help/family-online-safety/online-safety/UK-age-verification">UK age verification on Xbox</a>',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916233',
|
||||
whatHappened: "This account doesn't have an Xbox profile set up or doesn't own Minecraft.",
|
||||
stepsToFix: [
|
||||
'Make sure Minecraft is purchased on this account',
|
||||
'Visit <a href="https://www.minecraft.net/en-us/login">Minecraft Login</a> and sign in',
|
||||
'Complete Xbox profile setup if prompted',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916235',
|
||||
whatHappened: "Xbox Live isn't available in your region, so sign-in is blocked.",
|
||||
stepsToFix: [
|
||||
'Xbox services must be supported in your country before you can sign in',
|
||||
'Check <a href="https://www.xbox.com/en-US/regions">Xbox Availability</a> for supported regions',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916236',
|
||||
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||
'Complete the identity verification process',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916237',
|
||||
whatHappened: 'This account requires adult verification under South Korean regulations.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||
'Complete the identity verification process',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916238',
|
||||
whatHappened: 'This account is underage and not linked to a Microsoft family group.',
|
||||
stepsToFix: [
|
||||
'Review the <a href="https://help.minecraft.net/hc/en-us/articles/4408968616077">Family Setup Guide</a>',
|
||||
'Join or create a family group as instructed',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916227',
|
||||
whatHappened: 'This account was suspended for violating Xbox Community Standards.',
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://support.xbox.com">Xbox Support</a> and review the enforcement details',
|
||||
'Submit an appeal if one is available',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916229',
|
||||
whatHappened: "This account is restricted and doesn't have permission to play online.",
|
||||
stepsToFix: [
|
||||
'Have a guardian sign in to <a href="https://account.microsoft.com/family/">Microsoft Family</a>',
|
||||
'Update online play permissions',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916234',
|
||||
whatHappened: "This account hasn't accepted Xbox's Terms of Service.",
|
||||
stepsToFix: [
|
||||
'Visit <a href="https://www.xbox.com">Xbox</a> and sign in',
|
||||
'Accept the Terms if prompted',
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -3,16 +3,23 @@ import {
|
||||
CoffeeIcon,
|
||||
GameIcon,
|
||||
GaugeIcon,
|
||||
LanguagesIcon,
|
||||
ModrinthIcon,
|
||||
PaintbrushIcon,
|
||||
ReportIcon,
|
||||
SettingsIcon,
|
||||
ShieldIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ProgressBar, TabbedModal } from '@modrinth/ui'
|
||||
import {
|
||||
commonMessages,
|
||||
defineMessage,
|
||||
defineMessages,
|
||||
ProgressBar,
|
||||
TabbedModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||
import { defineMessage, defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -20,6 +27,7 @@ import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||
import LanguageSettings from '@/components/ui/settings/LanguageSettings.vue'
|
||||
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
||||
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
@@ -46,6 +54,15 @@ const tabs = [
|
||||
icon: PaintbrushIcon,
|
||||
content: AppearanceSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.language',
|
||||
defaultMessage: 'Language',
|
||||
}),
|
||||
icon: LanguagesIcon,
|
||||
content: LanguageSettings,
|
||||
badge: commonMessages.beta,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.privacy',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
@@ -49,16 +49,16 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const emit = defineEmits(['proceed'])
|
||||
const modal = ref(null)
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
hide_ads_window()
|
||||
modal.value.show()
|
||||
modal.value?.show()
|
||||
},
|
||||
hide: () => {
|
||||
onModalHide()
|
||||
modal.value.hide()
|
||||
modal.value?.hide()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<NewModal ref="modal" :header="formatMessage(messages.installToPlay)" :closable="true">
|
||||
<div v-if="requiredContentProject" class="flex flex-col gap-6 max-w-[500px]">
|
||||
<Admonition type="info" :header="formatMessage(messages.contentRequired)">
|
||||
{{ formatMessage(messages.serverRequiresMods) }}
|
||||
</Admonition>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold text-contrast">{{
|
||||
formatMessage(messages.requiredModpack)
|
||||
}}</span>
|
||||
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="openViewContents">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 rounded-xl bg-surface-2 p-3">
|
||||
<Avatar
|
||||
:src="requiredContentProject.icon_url"
|
||||
:alt="requiredContentProject.title"
|
||||
size="48px"
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="font-semibold text-contrast">
|
||||
<template v-if="usingCustomModpack && modpackVersion">
|
||||
{{ modpackVersion.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ requiredContentProject.title }}
|
||||
</template>
|
||||
</span>
|
||||
<span class="text-sm text-secondary">
|
||||
{{ loaderDisplay }} {{ requiredContentProject.game_versions?.[0] }}
|
||||
<template v-if="modCount">
|
||||
· {{ formatMessage(messages.modCount, { count: modCount }) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled>
|
||||
<button @click="handleDecline">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleAccept">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
<ModpackContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="project?.name ?? ''"
|
||||
:modpack-icon-url="project?.icon_url ?? undefined"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { DownloadIcon, EyeIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
formatLoader,
|
||||
ModpackContentModal,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
|
||||
import { get_project, get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import { installServerProject, useInstall } from '@/store/install.js'
|
||||
|
||||
import type { ContentItem } from '../../../../../../packages/ui/src/components/instances/types'
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const modpackVersionId = ref<string | null>(null)
|
||||
const modpackVersion = ref<Labrinth.Versions.v2.Version | null>(null)
|
||||
const project = ref<Labrinth.Projects.v3.Project | null>(null)
|
||||
const requiredContentProject = ref<Labrinth.Projects.v2.Project | null>(null)
|
||||
const onInstallComplete = ref<() => void>(() => {})
|
||||
const { formatMessage } = useVIntl()
|
||||
const installStore = useInstall()
|
||||
|
||||
const usingCustomModpack = computed(() => {
|
||||
return requiredContentProject.value?.id === project.value?.id
|
||||
})
|
||||
|
||||
const loaderDisplay = computed(() => {
|
||||
const loader = requiredContentProject.value?.loaders?.[0]
|
||||
if (!loader) return ''
|
||||
return formatLoader(formatMessage, loader)
|
||||
})
|
||||
|
||||
const modCount = computed(() => modpackVersion.value?.dependencies?.length)
|
||||
|
||||
async function fetchData(versionId: string) {
|
||||
// cache is making version null for some reason so bypassing for now
|
||||
modpackVersion.value = await get_version(versionId, 'bypass')
|
||||
|
||||
if (modpackVersion.value?.project_id) {
|
||||
requiredContentProject.value = await get_project(modpackVersion.value.project_id, 'bypass')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAccept() {
|
||||
hide()
|
||||
const serverProjectId = project.value?.id
|
||||
installStore.startInstallingServer(serverProjectId)
|
||||
try {
|
||||
await installServerProject(serverProjectId)
|
||||
onInstallComplete.value()
|
||||
} catch (error) {
|
||||
console.error('Failed to install server project from InstallToPlayModal:', error)
|
||||
} finally {
|
||||
installStore.stopInstallingServer(serverProjectId)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDecline() {
|
||||
hide()
|
||||
}
|
||||
|
||||
const modpackContentModal = ref<InstanceType<typeof ModpackContentModal>>()
|
||||
|
||||
async function openViewContents() {
|
||||
modpackContentModal.value?.showLoading()
|
||||
try {
|
||||
// Ensure version data is available — the useQuery may not have resolved yet
|
||||
const versionId = modpackVersionId.value
|
||||
const version =
|
||||
modpackVersion.value ?? (versionId ? await get_version(versionId, 'must_revalidate') : null)
|
||||
|
||||
const deps = version?.dependencies ?? []
|
||||
|
||||
const projectIds = deps
|
||||
.map((d: { project_id?: string }) => d.project_id)
|
||||
.filter((id: string | undefined): id is string => !!id)
|
||||
|
||||
const versionIds = deps
|
||||
.map((d: { version_id?: string }) => d.version_id)
|
||||
.filter((id: string | undefined): id is string => !!id)
|
||||
|
||||
const projects: Labrinth.Projects.v2.Project[] =
|
||||
projectIds.length > 0 ? await get_project_many(projectIds, 'must_revalidate') : []
|
||||
|
||||
const versions: Labrinth.Versions.v2.Version[] =
|
||||
versionIds.length > 0 ? await get_version_many(versionIds, 'must_revalidate') : []
|
||||
|
||||
const projectMap = new Map(projects.map((p: Labrinth.Projects.v2.Project) => [p.id, p]))
|
||||
|
||||
const contentItems: ContentItem[] = deps.map(
|
||||
(dep: Labrinth.Versions.v2.Dependency): ContentItem => {
|
||||
const depProject = dep.project_id ? projectMap.get(dep.project_id) : null
|
||||
// @ts-expect-error - version_id is missing from the type for some reason
|
||||
const depVersion = dep.version_id
|
||||
? // @ts-expect-error - version_id is missing from the type for some reason
|
||||
versions.find((v: Labrinth.Versions.v2.Version) => v.id === dep.version_id)
|
||||
: null
|
||||
|
||||
return {
|
||||
file_name: dep.file_name ?? depProject?.title ?? 'Unknown',
|
||||
project_type: depProject?.project_type ?? 'mod',
|
||||
has_update: false,
|
||||
update_version_id: null,
|
||||
project: {
|
||||
id: depProject?.id ?? dep.project_id ?? dep.file_name ?? 'unknown',
|
||||
slug: depProject?.slug ?? dep.project_id ?? 'unknown',
|
||||
title: depProject?.title ?? dep.file_name ?? 'Unknown',
|
||||
icon_url: depProject?.icon_url ?? undefined,
|
||||
},
|
||||
...(depVersion
|
||||
? {
|
||||
version: {
|
||||
id: depVersion.id,
|
||||
file_name: depVersion.files?.[0]?.filename ?? dep.file_name,
|
||||
version_number: depVersion.version_number ?? undefined,
|
||||
date_published: depVersion.date_published ?? undefined,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
},
|
||||
)
|
||||
modpackContentModal.value?.show(contentItems)
|
||||
} catch (err) {
|
||||
console.error('Failed to load modpack contents:', err)
|
||||
modpackContentModal.value?.show([])
|
||||
}
|
||||
}
|
||||
|
||||
async function show(
|
||||
projectVal: Labrinth.Projects.v3.Project,
|
||||
modpackVersionIdVal: string | null = null,
|
||||
callback: () => void = () => {},
|
||||
e?: MouseEvent,
|
||||
) {
|
||||
project.value = projectVal
|
||||
modpackVersionId.value = modpackVersionIdVal
|
||||
modpackVersion.value = null
|
||||
requiredContentProject.value = null
|
||||
onInstallComplete.value = callback
|
||||
|
||||
if (modpackVersionIdVal) await fetchData(modpackVersionIdVal)
|
||||
|
||||
hide_ads_window()
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
installToPlay: {
|
||||
id: 'app.modal.install-to-play.header',
|
||||
defaultMessage: 'Install to play',
|
||||
},
|
||||
sharedServerInstance: {
|
||||
id: 'app.modal.install-to-play.shared-server-instance',
|
||||
defaultMessage: 'Shared server instance',
|
||||
},
|
||||
contentRequired: {
|
||||
id: 'app.modal.install-to-play.content-required',
|
||||
defaultMessage: 'Content required',
|
||||
},
|
||||
serverRequiresMods: {
|
||||
id: 'app.modal.install-to-play.server-requires-mods',
|
||||
defaultMessage:
|
||||
'This server requires mods to play. Click Install to set up the required files from Modrinth, then launch directly into the server.',
|
||||
},
|
||||
requiredModpack: {
|
||||
id: 'app.modal.install-to-play.required-modpack',
|
||||
defaultMessage: 'Required modpack',
|
||||
},
|
||||
sharedInstance: {
|
||||
id: 'app.modal.install-to-play.shared-instance',
|
||||
defaultMessage: 'Shared instance',
|
||||
},
|
||||
modCount: {
|
||||
id: 'app.modal.install-to-play.mod-count',
|
||||
defaultMessage: '{count, plural, one {# mod} other {# mods}}',
|
||||
},
|
||||
installButton: {
|
||||
id: 'app.modal.install-to-play.install-button',
|
||||
defaultMessage: 'Install',
|
||||
},
|
||||
viewContents: {
|
||||
id: 'app.modal.install-to-play.view-contents',
|
||||
defaultMessage: 'View contents',
|
||||
},
|
||||
})
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
CodeIcon,
|
||||
@@ -7,10 +8,16 @@ import {
|
||||
MonitorIcon,
|
||||
WrenchIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
commonMessages,
|
||||
defineMessage,
|
||||
TabbedModal,
|
||||
type TabbedModalTab,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
||||
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||
@@ -18,14 +25,38 @@ 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 ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get_project_v3 } from '@/helpers/cache'
|
||||
|
||||
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<InstanceSettingsTabProps>()
|
||||
const emit = defineEmits<{
|
||||
unlinked: []
|
||||
}>()
|
||||
|
||||
const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
||||
const isMinecraftServer = ref(false)
|
||||
const handleUnlinked = () => emit('unlinked')
|
||||
|
||||
watch(
|
||||
() => props.instance,
|
||||
(instance) => {
|
||||
isMinecraftServer.value = false
|
||||
if (instance.linked_data?.project_id) {
|
||||
get_project_v3(instance.linked_data.project_id, 'must_revalidate')
|
||||
.then((project: Labrinth.Projects.v3.Project | undefined) => {
|
||||
if (project?.minecraft_server != null) {
|
||||
isMinecraftServer.value = true
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const tabs = computed<TabbedModalTab<InstanceSettingsTabProps>[]>(() => [
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'instance.settings.tabs.general',
|
||||
@@ -66,7 +97,7 @@ const tabs: TabbedModalTab<InstanceSettingsTabProps>[] = [
|
||||
icon: CodeIcon,
|
||||
content: HooksSettings,
|
||||
},
|
||||
]
|
||||
])
|
||||
|
||||
const modal = ref()
|
||||
|
||||
@@ -75,11 +106,6 @@ function show() {
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
|
||||
const titleMessage = defineMessage({
|
||||
id: 'instance.settings.title',
|
||||
defaultMessage: 'Settings',
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper ref="modal">
|
||||
@@ -91,10 +117,23 @@ const titleMessage = defineMessage({
|
||||
:tint-by="props.instance.path"
|
||||
/>
|
||||
{{ instance.name }} <ChevronRightIcon />
|
||||
<span class="font-extrabold text-contrast">{{ formatMessage(titleMessage) }}</span>
|
||||
<span class="font-extrabold text-contrast">{{
|
||||
formatMessage(commonMessages.settingsLabel)
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<TabbedModal :tabs="tabs.map((tab) => ({ ...tab, props }))" />
|
||||
<TabbedModal
|
||||
:tabs="
|
||||
tabs.map((tab) => ({
|
||||
...tab,
|
||||
props: {
|
||||
...props,
|
||||
isMinecraftServer,
|
||||
onUnlinked: handleUnlinked,
|
||||
},
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<NewModal
|
||||
ref="modal"
|
||||
:header="formatMessage(messages.updateToPlay)"
|
||||
:closable="true"
|
||||
no-padding
|
||||
@hide="() => show_ads_window()"
|
||||
>
|
||||
<div v-if="instance" class="max-w-[500px]">
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<Admonition type="info" :header="formatMessage(messages.updateRequired)">
|
||||
{{ formatMessage(messages.updateRequiredDescription, { name: instance.name }) }}
|
||||
</Admonition>
|
||||
|
||||
<div v-if="diffs.length" class="flex flex-col gap-2">
|
||||
<span v-if="publishedDate" class="text-contrast font-semibold">{{
|
||||
formatDate(publishedDate)
|
||||
}}</span>
|
||||
<div class="flex gap-2">
|
||||
<div v-if="removedCount" class="flex gap-1 items-center">
|
||||
<MinusIcon />
|
||||
{{ formatMessage(messages.removedCount, { count: removedCount }) }}
|
||||
</div>
|
||||
<div v-if="addedCount" class="flex gap-1 items-center">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addedCount, { count: addedCount }) }}
|
||||
</div>
|
||||
<div v-if="updatedCount" class="flex gap-1 items-center">
|
||||
<RefreshCwIcon />
|
||||
{{ formatMessage(messages.updatedCount, { count: updatedCount }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="diffs.length"
|
||||
class="flex flex-col bg-surface-2 p-4 max-h-[272px] overflow-y-auto border-t border-b border-r-0 border-l-0 border-solid border-surface-5"
|
||||
>
|
||||
<div
|
||||
v-for="(diff, index) in diffs"
|
||||
:key="diff.project_id"
|
||||
class="grid items-center min-h-10 h-10 gap-2"
|
||||
:class="diff.project?.title ? 'grid-cols-[auto_1fr_1fr_1fr]' : 'grid-cols-[auto_1fr_1fr]'"
|
||||
>
|
||||
<div class="flex flex-col justify-between items-center">
|
||||
<div class="w-[1px] h-2"></div>
|
||||
<PlusIcon v-if="diff.type === 'added'" />
|
||||
<MinusIcon v-else-if="diff.type === 'removed'" />
|
||||
<RefreshCwIcon v-else />
|
||||
<div
|
||||
:class="index === diffs.length - 1 ? 'bg-transparent' : 'bg-surface-5'"
|
||||
class="w-[1px] h-2 relative top-1"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 col-span-2">
|
||||
<span class="text-sm">{{ formatMessage(diffTypeMessages[diff.type]) }}</span>
|
||||
<span
|
||||
v-if="diff.project"
|
||||
v-tooltip="diff.project.title"
|
||||
class="text-sm text-contrast font-medium truncate"
|
||||
>
|
||||
{{ diff.project.title }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="diff.fileName"
|
||||
v-tooltip="diff.fileName"
|
||||
class="text-sm text-contrast font-medium truncate"
|
||||
>
|
||||
{{ decodeURIComponent(diff.fileName) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="
|
||||
diff.project?.title &&
|
||||
(getFilename(diff.newVersion) || getFilename(diff.currentVersion) || diff.fileName)
|
||||
"
|
||||
v-tooltip="
|
||||
getFilename(diff.newVersion) ||
|
||||
getFilename(diff.currentVersion) ||
|
||||
decodeURIComponent(diff.fileName || '')
|
||||
"
|
||||
class="text-xs truncate text-right"
|
||||
>
|
||||
{{
|
||||
getFilename(diff.newVersion) ||
|
||||
getFilename(diff.currentVersion) ||
|
||||
decodeURIComponent(diff.fileName || '')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex justify-between gap-2">
|
||||
<ButtonStyled color="red" type="transparent">
|
||||
<button @click="handleReport">
|
||||
<ReportIcon />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button @click="handleDecline">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleUpdate">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(commonMessages.updateButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
DownloadIcon,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
ReportIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
useFormatDateTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
|
||||
import { get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { useInstall } from '@/store/install.js'
|
||||
|
||||
type Dependency = Labrinth.Versions.v3.Dependency
|
||||
type Version = Labrinth.Versions.v2.Version
|
||||
|
||||
interface BaseDiff {
|
||||
project_id: string
|
||||
project?: {
|
||||
title: string
|
||||
icon_url?: string
|
||||
slug: string
|
||||
}
|
||||
currentVersionId?: string
|
||||
newVersionId?: string
|
||||
currentVersion?: Version
|
||||
newVersion?: Version
|
||||
fileName?: string
|
||||
}
|
||||
interface AddedDiff extends BaseDiff {
|
||||
type: 'added'
|
||||
newVersionId: string
|
||||
}
|
||||
interface RemovedDiff extends BaseDiff {
|
||||
type: 'removed'
|
||||
}
|
||||
interface UpdatedDiff extends BaseDiff {
|
||||
type: 'updated'
|
||||
currentVersionId: string
|
||||
newVersionId: string
|
||||
}
|
||||
type DependencyDiff = AddedDiff | RemovedDiff | UpdatedDiff
|
||||
|
||||
type ProjectInfo = {
|
||||
id: string
|
||||
title: string
|
||||
icon_url?: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatDate = useFormatDateTime({ dateStyle: 'long' })
|
||||
const installStore = useInstall()
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const instance = ref<GameInstance | null>(null)
|
||||
const onUpdateComplete = ref<UpdateCompleteCallback>(() => {})
|
||||
const diffs = ref<DependencyDiff[]>([])
|
||||
const modpackVersionId = ref<string | null>(null)
|
||||
const modpackVersion = ref<Version | null>(null)
|
||||
|
||||
const removedCount = computed(() => diffs.value.filter((d) => d.type === 'removed').length)
|
||||
const addedCount = computed(() => diffs.value.filter((d) => d.type === 'added').length)
|
||||
const updatedCount = computed(() => diffs.value.filter((d) => d.type === 'updated').length)
|
||||
const publishedDate = computed(() =>
|
||||
modpackVersion.value?.date_published ? new Date(modpackVersion.value.date_published) : null,
|
||||
)
|
||||
|
||||
function getFilename(version?: Version): string | undefined {
|
||||
return version?.files.find((f) => f.primary)?.filename
|
||||
}
|
||||
|
||||
async function computeDependencyDiffs(
|
||||
currentDeps: Dependency[],
|
||||
latestDeps: Dependency[],
|
||||
): Promise<DependencyDiff[]> {
|
||||
console.log('Computing dependency diffs', { currentDeps, latestDeps })
|
||||
|
||||
// Separate deps with project_id from file_name-only deps
|
||||
const currentWithProject = currentDeps.filter((d) => d.project_id)
|
||||
const latestWithProject = latestDeps.filter((d) => d.project_id)
|
||||
const currentFileOnly = currentDeps.filter((d) => !d.project_id && d.file_name)
|
||||
const latestFileOnly = latestDeps.filter((d) => !d.project_id && d.file_name)
|
||||
|
||||
const currentByProject = new Map<string, Dependency>(
|
||||
currentWithProject.map((d) => [d.project_id!, d]),
|
||||
)
|
||||
const latestByProject = new Map<string, Dependency>(
|
||||
latestWithProject.map((d) => [d.project_id!, d]),
|
||||
)
|
||||
const currentFilenames = new Set(currentFileOnly.map((d) => d.file_name!))
|
||||
const latestFilenames = new Set(latestFileOnly.map((d) => d.file_name!))
|
||||
|
||||
const diffs: DependencyDiff[] = []
|
||||
|
||||
// Find added and updated dependencies (by project_id)
|
||||
latestByProject.forEach((latestDep, projectId) => {
|
||||
const currentDep = currentByProject.get(projectId)
|
||||
if (!currentDep && latestDep.version_id) {
|
||||
diffs.push({ type: 'added', project_id: projectId, newVersionId: latestDep.version_id })
|
||||
} else if (
|
||||
currentDep?.version_id &&
|
||||
latestDep?.version_id &&
|
||||
currentDep?.version_id !== latestDep.version_id
|
||||
) {
|
||||
diffs.push({
|
||||
type: 'updated',
|
||||
project_id: projectId,
|
||||
currentVersionId: currentDep.version_id,
|
||||
newVersionId: latestDep.version_id,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Find removed dependencies (by project_id)
|
||||
currentByProject.forEach((currentDep, projectId) => {
|
||||
if (!latestByProject.has(projectId)) {
|
||||
diffs.push({
|
||||
type: 'removed',
|
||||
project_id: projectId,
|
||||
currentVersionId: currentDep.version_id,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Find added/removed file_name-only dependencies
|
||||
// ideally in future, this should use the hash of the file instead of filename, but since version dependencies don't include file hashes, we'll use filename as a best effort approach
|
||||
for (const fileName of latestFilenames) {
|
||||
if (!currentFilenames.has(fileName)) {
|
||||
diffs.push({ type: 'added', project_id: '', newVersionId: '' as string, fileName })
|
||||
}
|
||||
}
|
||||
for (const fileName of currentFilenames) {
|
||||
if (!latestFilenames.has(fileName)) {
|
||||
diffs.push({ type: 'removed', project_id: '', fileName })
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch projects and versions of diffs
|
||||
const allProjectIds = [...new Set(diffs.map((d) => d.project_id).filter(Boolean))]
|
||||
const allVersionIds = [
|
||||
...new Set(
|
||||
[...diffs.map((d) => d.newVersionId), ...diffs.map((d) => d.currentVersionId)].filter(
|
||||
Boolean,
|
||||
),
|
||||
),
|
||||
] as string[]
|
||||
const [projects, versions] = await Promise.all([
|
||||
get_project_many(allProjectIds, 'bypass'),
|
||||
get_version_many(allVersionIds, 'bypass'),
|
||||
])
|
||||
|
||||
const projectMap = new Map<string, ProjectInfo>(projects.map((p: ProjectInfo) => [p.id, p]))
|
||||
const versionMap = new Map<string, Version>(versions.map((v: Version) => [v.id, v]))
|
||||
|
||||
const mappedDiffs = diffs
|
||||
.map((diff) => {
|
||||
const project = projectMap.get(diff.project_id)
|
||||
return {
|
||||
...diff,
|
||||
project: project
|
||||
? { title: project.title, icon_url: project.icon_url, slug: project.slug }
|
||||
: undefined,
|
||||
currentVersion: diff.currentVersionId ? versionMap.get(diff.currentVersionId) : undefined,
|
||||
newVersion: diff.newVersionId ? versionMap.get(diff.newVersionId) : undefined,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const typeOrder = { removed: 0, added: 1, updated: 2 }
|
||||
const typeCompare = typeOrder[a.type] - typeOrder[b.type]
|
||||
if (typeCompare !== 0) return typeCompare
|
||||
|
||||
const aDate = a.newVersion?.date_published || a.currentVersion?.date_published || ''
|
||||
const bDate = b.newVersion?.date_published || b.currentVersion?.date_published || ''
|
||||
return dayjs(bDate).valueOf() - dayjs(aDate).valueOf()
|
||||
})
|
||||
.filter((d) => d.project || d.fileName) // filter out any diffs that couldn't be matched to a project or file
|
||||
return mappedDiffs
|
||||
}
|
||||
|
||||
async function checkUpdateAvailable(inst: GameInstance): Promise<DependencyDiff[] | null> {
|
||||
if (!inst.linked_data) return null
|
||||
if (!modpackVersionId.value || !inst.linked_data.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.
|
||||
modpackVersion.value = await get_version(modpackVersionId.value, 'bypass')
|
||||
const instanceModpackVersion = await get_version(inst.linked_data.version_id, 'bypass')
|
||||
|
||||
// Compute dependency diffs between current and latest version
|
||||
if (instanceModpackVersion && modpackVersion.value) {
|
||||
return await computeDependencyDiffs(
|
||||
instanceModpackVersion.dependencies || [],
|
||||
modpackVersion.value.dependencies || [],
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error)
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => instance.value,
|
||||
async (newInstance) => {
|
||||
if (!newInstance) return
|
||||
const result = await checkUpdateAvailable(newInstance)
|
||||
diffs.value = result || []
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
async function handleUpdate() {
|
||||
hide()
|
||||
const serverProjectId = instance.value?.linked_data?.project_id
|
||||
if (serverProjectId) installStore.startInstallingServer(serverProjectId)
|
||||
try {
|
||||
if (modpackVersionId.value && instance.value) {
|
||||
await update_managed_modrinth_version(instance.value.path, modpackVersionId.value)
|
||||
await onUpdateComplete.value()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating instance:', error)
|
||||
} finally {
|
||||
if (serverProjectId) installStore.stopInstallingServer(serverProjectId)
|
||||
}
|
||||
}
|
||||
|
||||
function handleReport() {
|
||||
if (instance.value?.linked_data?.project_id) {
|
||||
openUrl(
|
||||
`https://modrinth.com/report?item=project&itemID=${instance.value.linked_data.project_id}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDecline() {
|
||||
hide()
|
||||
}
|
||||
|
||||
function show(
|
||||
instanceVal: GameInstance,
|
||||
modpackVersionIdVal: string | null = null,
|
||||
callback: UpdateCompleteCallback = () => {},
|
||||
e?: MouseEvent,
|
||||
) {
|
||||
instance.value = instanceVal
|
||||
modpackVersionId.value = modpackVersionIdVal
|
||||
onUpdateComplete.value = callback
|
||||
hide_ads_window()
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
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.',
|
||||
},
|
||||
removedCount: {
|
||||
id: 'app.modal.update-to-play.removed-count',
|
||||
defaultMessage: '{count} removed',
|
||||
},
|
||||
addedCount: {
|
||||
id: 'app.modal.update-to-play.added-count',
|
||||
defaultMessage: '{count} added',
|
||||
},
|
||||
updatedCount: {
|
||||
id: 'app.modal.update-to-play.updated-count',
|
||||
defaultMessage: '{count} updated',
|
||||
},
|
||||
})
|
||||
|
||||
const diffTypeMessages = defineMessages({
|
||||
added: {
|
||||
id: 'app.modal.update-to-play.diff-type.added',
|
||||
defaultMessage: 'Added',
|
||||
},
|
||||
removed: {
|
||||
id: 'app.modal.update-to-play.diff-type.removed',
|
||||
defaultMessage: 'Removed',
|
||||
},
|
||||
updated: {
|
||||
id: 'app.modal.update-to-play.diff-type.updated',
|
||||
defaultMessage: 'Updated',
|
||||
},
|
||||
})
|
||||
|
||||
const hasUpdate = computed(() => {
|
||||
if (!instance.value?.linked_data) return false
|
||||
return (
|
||||
modpackVersionId.value != null &&
|
||||
modpackVersionId.value !== instance.value.linked_data.version_id
|
||||
)
|
||||
})
|
||||
|
||||
defineExpose({ show, hide, hasUpdate })
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||
import { Combobox, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
@@ -50,7 +50,7 @@ watch(
|
||||
:model-value="themeStore.advancedRendering"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.advancedRendering = e
|
||||
themeStore.advancedRendering = !!e
|
||||
settings.advanced_rendering = themeStore.advancedRendering
|
||||
}
|
||||
"
|
||||
@@ -86,12 +86,13 @@ watch(
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Default landing page</h2>
|
||||
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
||||
</div>
|
||||
<TeleportDropdownMenu
|
||||
<Combobox
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="w-40"
|
||||
:options="['Home', 'Library']"
|
||||
:options="['Home', 'Library'].map((v) => ({ value: v, label: v }))"
|
||||
:display-value="settings.default_page ?? 'Select an option'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +123,7 @@ watch(
|
||||
:model-value="settings.toggle_sidebar"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.toggle_sidebar = e
|
||||
settings.toggle_sidebar = !!e
|
||||
themeStore.toggleSidebar = settings.toggle_sidebar
|
||||
}
|
||||
"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { injectNotificationManager, Slider, Toggle } from '@modrinth/ui'
|
||||
import { injectNotificationManager, Slider, StyledInput, Toggle } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
@@ -73,7 +73,7 @@ watch(
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
<StyledInput
|
||||
id="width"
|
||||
v-model="settings.game_resolution[0]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
@@ -91,13 +91,12 @@ watch(
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
<StyledInput
|
||||
id="height"
|
||||
v-model="settings.game_resolution[1]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
class="input"
|
||||
placeholder="Enter height..."
|
||||
/>
|
||||
</div>
|
||||
@@ -118,23 +117,23 @@ watch(
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
||||
<input
|
||||
<StyledInput
|
||||
id="java-args"
|
||||
v-model="settings.launchArgs"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter java arguments..."
|
||||
class="w-full"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
||||
<input
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="settings.envVars"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter environmental variables..."
|
||||
class="w-full"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||
@@ -143,37 +142,37 @@ watch(
|
||||
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Pre launch</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran before the instance is launched.</p>
|
||||
<input
|
||||
<StyledInput
|
||||
id="pre-launch"
|
||||
v-model="settings.hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter pre-launch command..."
|
||||
class="w-full"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Wrapper</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
Wrapper command for launching Minecraft.
|
||||
</p>
|
||||
<input
|
||||
<StyledInput
|
||||
id="wrapper"
|
||||
v-model="settings.hooks.wrapper"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter wrapper command..."
|
||||
class="w-full"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Post exit</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">Ran after the game closes.</p>
|
||||
<input
|
||||
<StyledInput
|
||||
id="post-exit"
|
||||
v-model="settings.hooks.post_exit"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter post-exit command..."
|
||||
class="w-full"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,7 +21,7 @@ async function updateJavaVersion(version) {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div v-for="(javaVersion, index) in [21, 17, 8]" :key="`java-${javaVersion}`">
|
||||
<div v-for="(javaVersion, index) in [25, 21, 17, 8]" :key="`java-${javaVersion}`">
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
||||
Java {{ javaVersion }} location
|
||||
</h2>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Admonition,
|
||||
AutoLink,
|
||||
IntlFormatted,
|
||||
LanguageSelector,
|
||||
languageSelectorMessages,
|
||||
LOCALES,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import i18n from '@/i18n.config'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const platform = computed(() => formatMessage(languageSelectorMessages.platformApp))
|
||||
|
||||
const settings = ref(await get())
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const $isChanging = ref(false)
|
||||
|
||||
async function onLocaleChange(newLocale: string) {
|
||||
if (settings.value.locale === newLocale) return
|
||||
|
||||
$isChanging.value = true
|
||||
try {
|
||||
i18n.global.locale.value = newLocale
|
||||
settings.value.locale = newLocale
|
||||
} finally {
|
||||
$isChanging.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Language</h2>
|
||||
|
||||
<Admonition type="warning" class="mt-2 mb-4">
|
||||
{{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
|
||||
</Admonition>
|
||||
|
||||
<p class="m-0 mb-4">
|
||||
<IntlFormatted
|
||||
:message-id="languageSelectorMessages.languagesDescription"
|
||||
:values="{ platform }"
|
||||
>
|
||||
<template #~crowdin-link="{ children }">
|
||||
<AutoLink to="https://translate.modrinth.com">
|
||||
<component :is="() => children" />
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</p>
|
||||
|
||||
<LanguageSelector
|
||||
:current-locale="settings.locale"
|
||||
:locales="LOCALES"
|
||||
:on-locale-change="onLocaleChange"
|
||||
:is-changing="$isChanging"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||
import { Button, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
@@ -28,10 +28,12 @@ watch(
|
||||
async function purgeCache() {
|
||||
await purge_cache_types([
|
||||
'project',
|
||||
'project_v3',
|
||||
'version',
|
||||
'user',
|
||||
'team',
|
||||
'organization',
|
||||
'file',
|
||||
'loader_manifest',
|
||||
'minecraft_manifest',
|
||||
'categories',
|
||||
@@ -39,8 +41,10 @@ async function purgeCache() {
|
||||
'loaders',
|
||||
'game_versions',
|
||||
'donation_platforms',
|
||||
'file_hash',
|
||||
'file_update',
|
||||
'search_results',
|
||||
'search_results_v3',
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
@@ -65,13 +69,19 @@ async function findLauncherDir() {
|
||||
</p>
|
||||
|
||||
<div class="m-1 my-2">
|
||||
<div class="iconified-input w-full">
|
||||
<BoxIcon />
|
||||
<input id="appDir" v-model="settings.custom_dir" type="text" class="input" />
|
||||
<Button class="r-btn" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<StyledInput
|
||||
id="appDir"
|
||||
v-model="settings.custom_dir"
|
||||
:icon="BoxIcon"
|
||||
type="text"
|
||||
wrapper-class="w-full"
|
||||
>
|
||||
<template #right>
|
||||
<Button class="r-btn" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
</template>
|
||||
</StyledInput>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -14,13 +14,13 @@ import {
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { capitalizeString } from '@modrinth/utils'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -36,6 +36,10 @@ import { handleSevereError } from '@/store/error'
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -80,7 +84,7 @@ const play = async (event: MouseEvent) => {
|
||||
await run(props.instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: props.instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
source: 'InstanceItem',
|
||||
@@ -130,7 +134,7 @@ onUnmounted(() => {
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised rounded-xl smart-clickable:highlight-on-hover"
|
||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow rounded-xl smart-clickable:highlight-on-hover"
|
||||
>
|
||||
<Avatar
|
||||
:src="instanceIcon ? convertFileSrc(instanceIcon) : undefined"
|
||||
@@ -145,18 +149,14 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-secondary">
|
||||
<div
|
||||
v-tooltip="
|
||||
instance.last_played
|
||||
? dayjs(instance.last_played).format('MMMM D, YYYY [at] h:mm A')
|
||||
: null
|
||||
"
|
||||
v-tooltip="instance.last_played ? formatDateTime(instance.last_played) : null"
|
||||
class="w-fit shrink-0"
|
||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
|
||||
>
|
||||
<template v-if="last_played">
|
||||
{{
|
||||
formatMessage(commonMessages.playedLabel, {
|
||||
time: formatRelativeTime(last_played.toISOString?.()),
|
||||
ago: formatRelativeTime(last_played.toISOString?.()),
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { LoaderCircleIcon } from '@modrinth/assets'
|
||||
import type { GameVersion } from '@modrinth/ui'
|
||||
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
@@ -39,6 +41,7 @@ const props = defineProps<{
|
||||
const theme = useTheming()
|
||||
|
||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||
const loading = ref(true)
|
||||
const serverData = ref<Record<string, ServerData>>({})
|
||||
const protocolVersions = ref<Record<string, ProtocolVersion | null>>({})
|
||||
const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []))
|
||||
@@ -46,6 +49,11 @@ const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []
|
||||
const MIN_JUMP_BACK_IN = 3
|
||||
const MAX_JUMP_BACK_IN = 6
|
||||
const TWO_WEEKS_AGO = dayjs().subtract(14, 'day')
|
||||
const MAX_LINUX_POPULATES = 3
|
||||
|
||||
// Track populate calls on Linux to prevent server ping spam
|
||||
const isLinux = platform() === 'linux'
|
||||
const linuxPopulateCount = ref(0)
|
||||
|
||||
type BaseJumpBackInItem = {
|
||||
last_played: Dayjs
|
||||
@@ -71,11 +79,19 @@ watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||
})
|
||||
})
|
||||
|
||||
await populateJumpBackIn().catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
populateJumpBackIn()
|
||||
.catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
async function populateJumpBackIn() {
|
||||
// On Linux, limit automatic populates to prevent server ping spam
|
||||
if (isLinux && linuxPopulateCount.value >= MAX_LINUX_POPULATES) return
|
||||
if (isLinux) linuxPopulateCount.value++
|
||||
|
||||
console.info('Repopulating jump back in...')
|
||||
|
||||
const worldItems: WorldJumpBackInItem[] = []
|
||||
@@ -159,10 +175,17 @@ function refreshServer(address: string, instancePath: string) {
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||
}
|
||||
|
||||
async function joinWorld(world: WorldWithProfile) {
|
||||
async function joinWorld(world: WorldWithProfile, instance?: GameInstance) {
|
||||
console.log(`Joining world ${getWorldIdentifier(world)}`)
|
||||
if (world.type === 'server') {
|
||||
await start_join_server(world.profile, world.address).catch(handleError)
|
||||
if (instance) {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: instance.loader,
|
||||
game_version: instance.game_version,
|
||||
source: 'WorldItem',
|
||||
})
|
||||
}
|
||||
} else if (world.type === 'singleplayer') {
|
||||
await start_join_singleplayer_world(world.profile, world.path).catch(handleError)
|
||||
}
|
||||
@@ -172,7 +195,7 @@ async function playInstance(instance: GameInstance) {
|
||||
await run(instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
trackEvent('InstanceStart', {
|
||||
loader: instance.loader,
|
||||
game_version: instance.game_version,
|
||||
source: 'WorldItem',
|
||||
@@ -224,6 +247,7 @@ const checkProcesses = async () => {
|
||||
|
||||
onMounted(() => {
|
||||
checkProcesses()
|
||||
linuxPopulateCount.value = 0
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -233,7 +257,15 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||
<div v-if="loading" class="flex flex-col gap-2">
|
||||
<span class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold">
|
||||
Jump back in
|
||||
</span>
|
||||
<div class="text-center py-4">
|
||||
<LoaderCircleIcon class="mx-auto size-8 animate-spin text-contrast" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||
Jump back in
|
||||
</HeadingLink>
|
||||
@@ -292,7 +324,7 @@ onUnmounted(() => {
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
currentWorld = getWorldIdentifier(item.world)
|
||||
joinWorld(item.world)
|
||||
joinWorld(item.world, item.instance)
|
||||
}
|
||||
"
|
||||
@play-instance="
|
||||
|
||||
@@ -17,18 +17,22 @@ import {
|
||||
UserIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { MessageDescriptor } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
TagItem,
|
||||
useFormatDateTime,
|
||||
useFormatNumber,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||
import { getPingLevel } from '@modrinth/utils'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import type { MessageDescriptor } from '@vintl/vintl'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import dayjs from 'dayjs'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import type { Component } from 'vue'
|
||||
@@ -45,8 +49,15 @@ import type {
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { getWorldIdentifier, set_world_display_status } from '@/helpers/worlds.ts'
|
||||
|
||||
import { LockIcon } from '../../../../../../packages/assets/generated-icons'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const formatNumber = useFormatNumber()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -77,6 +88,8 @@ const props = withDefaults(
|
||||
message: MessageDescriptor
|
||||
}
|
||||
|
||||
managed?: boolean
|
||||
|
||||
// Instance
|
||||
instancePath?: string
|
||||
instanceName?: string
|
||||
@@ -95,6 +108,7 @@ const props = withDefaults(
|
||||
renderedMotd: undefined,
|
||||
|
||||
gameMode: undefined,
|
||||
managed: false,
|
||||
|
||||
instancePath: undefined,
|
||||
instanceName: undefined,
|
||||
@@ -116,6 +130,7 @@ const serverIncompatible = computed(
|
||||
)
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
const managed = computed(() => props.managed)
|
||||
|
||||
const messages = defineMessages({
|
||||
hardcore: {
|
||||
@@ -170,6 +185,10 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.dont_show_on_home',
|
||||
defaultMessage: `Don't show on Home`,
|
||||
},
|
||||
linkedServer: {
|
||||
id: 'instance.worlds.linked_server',
|
||||
defaultMessage: 'Managed by server project',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
@@ -181,14 +200,16 @@ const messages = defineMessages({
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised smart-clickable:highlight-on-hover rounded-xl"
|
||||
class="grid grid-cols-[auto_minmax(0,3fr)_minmax(0,4fr)_auto] items-center gap-2 p-3 bg-bg-raised card-shadow smart-clickable:highlight-on-hover rounded-xl"
|
||||
:class="{
|
||||
'world-item-highlighted': highlighted,
|
||||
}"
|
||||
>
|
||||
<Avatar
|
||||
:src="
|
||||
world.type === 'server' && serverStatus ? serverStatus.favicon ?? world.icon : world.icon
|
||||
world.type === 'server' && serverStatus
|
||||
? (serverStatus.favicon ?? world.icon)
|
||||
: world.icon
|
||||
"
|
||||
size="48px"
|
||||
/>
|
||||
@@ -197,6 +218,14 @@ const messages = defineMessages({
|
||||
<div class="text-lg text-contrast font-bold truncate smart-clickable:underline-on-hover">
|
||||
{{ world.name }}
|
||||
</div>
|
||||
<TagItem
|
||||
v-if="managed"
|
||||
v-tooltip="formatMessage(messages.linkedServer)"
|
||||
class="border !border-solid border-blue bg-highlight-blue text-xs"
|
||||
:style="`--_color: var(--color-blue)`"
|
||||
>
|
||||
<LockIcon aria-hidden="true" class="h-5 w-5" />
|
||||
</TagItem>
|
||||
<div
|
||||
v-if="world.type === 'singleplayer'"
|
||||
class="text-sm text-secondary flex items-center gap-1 font-semibold"
|
||||
@@ -236,7 +265,7 @@ const messages = defineMessages({
|
||||
/>
|
||||
<Tooltip :disabled="!hasPlayersTooltip">
|
||||
<span :class="{ 'cursor-help': hasPlayersTooltip }">
|
||||
{{ formatNumber(serverStatus.players?.online, false) }}
|
||||
{{ formatNumber(serverStatus.players?.online) }}
|
||||
online
|
||||
</span>
|
||||
<template #popper>
|
||||
@@ -257,9 +286,7 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-secondary">
|
||||
<div
|
||||
v-tooltip="
|
||||
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
||||
"
|
||||
v-tooltip="world.last_played ? formatDateTime(world.last_played) : null"
|
||||
class="w-fit shrink-0"
|
||||
:class="{
|
||||
'cursor-help smart-clickable:allow-pointer-events': world.last_played,
|
||||
@@ -268,7 +295,7 @@ const messages = defineMessages({
|
||||
<template v-if="world.last_played">
|
||||
{{
|
||||
formatMessage(commonMessages.playedLabel, {
|
||||
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
||||
ago: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
@@ -393,8 +420,12 @@ const messages = defineMessages({
|
||||
id: 'edit',
|
||||
action: () => emit('edit'),
|
||||
shown: !instancePath,
|
||||
disabled: locked,
|
||||
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
@@ -429,8 +460,12 @@ const messages = defineMessages({
|
||||
hoverFilled: true,
|
||||
action: () => emit('delete'),
|
||||
shown: !instancePath,
|
||||
disabled: locked,
|
||||
tooltip: locked ? formatMessage(messages.worldInUse) : undefined,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessage,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -97,12 +104,11 @@ const messages = defineMessages({
|
||||
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
||||
{{ formatMessage(messages.name) }}
|
||||
</h2>
|
||||
<input
|
||||
<StyledInput
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.placeholderName)"
|
||||
class="w-full"
|
||||
autocomplete="off"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Checkbox } from '@modrinth/ui'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { Checkbox, defineMessage, useVIntl } from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { TeleportDropdownMenu } from '@modrinth/ui'
|
||||
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||
import {
|
||||
Combobox,
|
||||
defineMessages,
|
||||
type MessageDescriptor,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import type { ServerPackStatus } from '@/helpers/worlds.ts'
|
||||
|
||||
@@ -53,33 +58,38 @@ defineExpose({ resourcePackOptions })
|
||||
<h2 class="text-lg font-extrabold text-contrast mt-0 mb-1">
|
||||
{{ formatMessage(messages.name) }}
|
||||
</h2>
|
||||
<input
|
||||
<StyledInput
|
||||
v-model="name"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.placeholderName)"
|
||||
class="w-full"
|
||||
autocomplete="off"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
||||
{{ formatMessage(messages.address) }}
|
||||
</h2>
|
||||
<input
|
||||
<StyledInput
|
||||
v-model="address"
|
||||
type="text"
|
||||
placeholder="example.modrinth.gg"
|
||||
class="w-full"
|
||||
autocomplete="off"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<h2 class="text-lg font-extrabold text-contrast mt-3 mb-1">
|
||||
{{ formatMessage(messages.resourcePack) }}
|
||||
</h2>
|
||||
<div>
|
||||
<TeleportDropdownMenu
|
||||
<Combobox
|
||||
v-model="resourcePack"
|
||||
:options="resourcePackOptions"
|
||||
:options="
|
||||
resourcePackOptions.map((o) => ({
|
||||
value: o,
|
||||
label: formatMessage(resourcePackOptionMessages[o]),
|
||||
}))
|
||||
"
|
||||
name="Server resource pack"
|
||||
:display-name="
|
||||
(option: ServerPackStatus) => formatMessage(resourcePackOptionMessages[option])
|
||||
:display-value="
|
||||
resourcePack
|
||||
? formatMessage(resourcePackOptionMessages[resourcePack])
|
||||
: 'Select an option'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { posthog } from 'posthog-js'
|
||||
|
||||
export const initAnalytics = () => {
|
||||
posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
||||
persistence: 'localStorage',
|
||||
api_host: 'https://posthog.modrinth.com',
|
||||
})
|
||||
}
|
||||
|
||||
export const debugAnalytics = () => {
|
||||
posthog.debug()
|
||||
}
|
||||
|
||||
export const optOutAnalytics = () => {
|
||||
posthog.opt_out_capturing()
|
||||
}
|
||||
|
||||
export const optInAnalytics = () => {
|
||||
posthog.opt_in_capturing()
|
||||
}
|
||||
|
||||
export const trackEvent = (eventName, properties) => {
|
||||
posthog.capture(eventName, properties)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { posthog } from 'posthog-js'
|
||||
|
||||
interface InstanceProperties {
|
||||
loader: string
|
||||
game_version: string
|
||||
}
|
||||
|
||||
interface ProjectProperties extends InstanceProperties {
|
||||
id: string
|
||||
project_type: string
|
||||
}
|
||||
|
||||
type AnalyticsEventMap = {
|
||||
Launched: { version: string; dev: boolean; onboarded: boolean }
|
||||
PageView: { path: string; fromPath: string; failed: unknown }
|
||||
InstanceCreate: { source: string }
|
||||
InstanceCreateStart: { source: string }
|
||||
InstanceStart: InstanceProperties & { source: string }
|
||||
InstanceStop: Partial<InstanceProperties> & { source?: string }
|
||||
InstanceDuplicate: InstanceProperties
|
||||
InstanceRepair: InstanceProperties
|
||||
InstanceSetIcon: Record<string, never>
|
||||
InstanceRemoveIcon: Record<string, never>
|
||||
InstanceUpdateAll: InstanceProperties & { count: number; selected: boolean }
|
||||
InstanceProjectUpdate: InstanceProperties & { id: string; name: string; project_type: string }
|
||||
InstanceProjectDisable: InstanceProperties & {
|
||||
id: string
|
||||
name: string
|
||||
project_type: string
|
||||
disabled: boolean
|
||||
}
|
||||
InstanceProjectRemove: InstanceProperties & { id: string; name: string; project_type: string }
|
||||
ProjectInstall: ProjectProperties & { version_id: string; title: string; source: string }
|
||||
ProjectInstallStart: { source: string }
|
||||
PackInstall: { id: string; version_id: string; title: string; source: string }
|
||||
PackInstallStart: Record<string, never>
|
||||
AccountLogIn: { source?: string }
|
||||
AccountLogOut: Record<string, never>
|
||||
JavaTest: { path: string; success: boolean }
|
||||
JavaManualSelect: { version: string }
|
||||
JavaAutoDetect: { path: string; version: string }
|
||||
}
|
||||
|
||||
export type AnalyticsEvent = keyof AnalyticsEventMap
|
||||
|
||||
let initialized = false
|
||||
|
||||
export const initAnalytics = () => {
|
||||
if (initialized) return
|
||||
posthog.init('phc_9Iqi6lFs9sr5BSqh9RRNRSJ0mATS9PSgirDiX3iOYJ', {
|
||||
persistence: 'localStorage',
|
||||
api_host: 'https://posthog.modrinth.com',
|
||||
})
|
||||
initialized = true
|
||||
}
|
||||
|
||||
export const debugAnalytics = () => {
|
||||
if (!initialized) return
|
||||
posthog.debug()
|
||||
}
|
||||
|
||||
export const optOutAnalytics = () => {
|
||||
if (!initialized) return
|
||||
posthog.opt_out_capturing()
|
||||
}
|
||||
|
||||
export const optInAnalytics = () => {
|
||||
initAnalytics()
|
||||
posthog.opt_in_capturing()
|
||||
}
|
||||
|
||||
type OptionalArgs<T> = Record<string, never> extends T ? [properties?: T] : [properties: T]
|
||||
|
||||
export const trackEvent = <E extends AnalyticsEvent>(
|
||||
eventName: E,
|
||||
...args: OptionalArgs<AnalyticsEventMap[E]>
|
||||
) => {
|
||||
if (!initialized) return
|
||||
posthog.capture(eventName, args[0])
|
||||
}
|
||||
@@ -13,6 +13,14 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
// await authenticate_await_completion()
|
||||
// }
|
||||
|
||||
/**
|
||||
* Check if the authentication servers are reachable, throwing an exception if
|
||||
* not reachable.
|
||||
*/
|
||||
export async function check_reachable() {
|
||||
await invoke('plugin:auth|check_reachable')
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user with Hydra - part 1.
|
||||
* This begins the authentication flow quasi-synchronously.
|
||||
|
||||
@@ -8,6 +8,14 @@ export async function get_project_many(ids, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_project_many', { ids, cacheBehaviour })
|
||||
}
|
||||
|
||||
export async function get_project_v3(id, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_project_v3', { id, cacheBehaviour })
|
||||
}
|
||||
|
||||
export async function get_project_v3_many(ids, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_project_v3_many', { ids, cacheBehaviour })
|
||||
}
|
||||
|
||||
export async function get_version(id, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_version', { id, cacheBehaviour })
|
||||
}
|
||||
@@ -48,6 +56,14 @@ export async function get_search_results_many(ids, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_search_results_many', { ids, cacheBehaviour })
|
||||
}
|
||||
|
||||
export async function get_search_results_v3(id, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_search_results_v3', { id, cacheBehaviour })
|
||||
}
|
||||
|
||||
export async function get_search_results_v3_many(ids, cacheBehaviour) {
|
||||
return await invoke('plugin:cache|get_search_results_v3_many', { ids, cacheBehaviour })
|
||||
}
|
||||
|
||||
export async function purge_cache_types(cacheTypes) {
|
||||
return await invoke('plugin:cache|purge_cache_types', { cacheTypes })
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
|
||||
export const useFetch = async (url, item, isSilent) => {
|
||||
try {
|
||||
const version = await getVersion()
|
||||
return await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
|
||||
})
|
||||
} catch (err) {
|
||||
if (!isSilent) {
|
||||
throw err
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,15 @@ import { install_to_existing_profile } from '@/helpers/pack.js'
|
||||
- icon is a path to an image file, which will be copied into the profile directory
|
||||
*/
|
||||
|
||||
export async function create(name, gameVersion, modloader, loaderVersion, iconPath, skipInstall) {
|
||||
export async function create(
|
||||
name,
|
||||
gameVersion,
|
||||
modloader,
|
||||
loaderVersion,
|
||||
icon,
|
||||
skipInstall,
|
||||
linkedData,
|
||||
) {
|
||||
//Trim string name to avoid "Unable to find directory"
|
||||
name = name.trim()
|
||||
return await invoke('plugin:profile-create|profile_create', {
|
||||
@@ -26,8 +34,9 @@ export async function create(name, gameVersion, modloader, loaderVersion, iconPa
|
||||
gameVersion,
|
||||
modloader,
|
||||
loaderVersion,
|
||||
iconPath,
|
||||
icon,
|
||||
skipInstall,
|
||||
linkedData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -174,8 +183,8 @@ export async function get_pack_export_candidates(profilePath) {
|
||||
|
||||
// Run Minecraft using a pathed profile
|
||||
// Returns PID of child
|
||||
export async function run(path) {
|
||||
return await invoke('plugin:profile|profile_run', { path })
|
||||
export async function run(path, serverAddress = null) {
|
||||
return await invoke('plugin:profile|profile_run', { path, serverAddress })
|
||||
}
|
||||
|
||||
export async function kill(path) {
|
||||
|
||||
@@ -36,6 +36,7 @@ export type AppSettings = {
|
||||
max_concurrent_writes: number
|
||||
|
||||
theme: ColorTheme
|
||||
locale: string
|
||||
default_page: 'home' | 'library'
|
||||
collapsed_navigation: boolean
|
||||
hide_nametag_skins_page: boolean
|
||||
@@ -67,6 +68,8 @@ export type AppSettings = {
|
||||
skipped_update: string | null
|
||||
pending_update_toast_for_version: string | null
|
||||
auto_download_updates: boolean | null
|
||||
|
||||
version: number
|
||||
}
|
||||
|
||||
// Get full settings object
|
||||
|
||||
+1
@@ -139,4 +139,5 @@ type AppSettings = {
|
||||
export type InstanceSettingsTabProps = {
|
||||
instance: GameInstance
|
||||
offline?: boolean
|
||||
isMinecraftServer?: boolean
|
||||
}
|
||||
|
||||
@@ -141,7 +141,12 @@ export async function add_server_to_profile(
|
||||
address: string,
|
||||
packStatus: ServerPackStatus,
|
||||
): Promise<number> {
|
||||
return await invoke('plugin:worlds|add_server_to_profile', { path, name, address, packStatus })
|
||||
return await invoke('plugin:worlds|add_server_to_profile', {
|
||||
path,
|
||||
name,
|
||||
address,
|
||||
packStatus,
|
||||
})
|
||||
}
|
||||
|
||||
export async function edit_server_in_profile(
|
||||
@@ -212,6 +217,150 @@ export function isServerWorld(world: World): world is ServerWorld {
|
||||
return world.type === 'server'
|
||||
}
|
||||
|
||||
const DEFAULT_MINECRAFT_SERVER_PORT = 25565
|
||||
|
||||
function parseServerPort(port: string): number | null {
|
||||
const parsed = Number.parseInt(port, 10)
|
||||
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null
|
||||
}
|
||||
|
||||
function parseServerHost(address: string): string {
|
||||
const trimmedAddress = address.trim()
|
||||
if (!trimmedAddress) return ''
|
||||
|
||||
if (trimmedAddress.startsWith('[')) {
|
||||
const closingBracket = trimmedAddress.indexOf(']')
|
||||
if (closingBracket > 0) {
|
||||
return trimmedAddress.slice(1, closingBracket).trim().toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
const firstColon = trimmedAddress.indexOf(':')
|
||||
const lastColon = trimmedAddress.lastIndexOf(':')
|
||||
|
||||
if (firstColon !== -1 && firstColon === lastColon) {
|
||||
return trimmedAddress.slice(0, firstColon).trim().toLowerCase()
|
||||
}
|
||||
|
||||
return trimmedAddress.toLowerCase()
|
||||
}
|
||||
|
||||
function isIPv4Host(host: string): boolean {
|
||||
const segments = host.split('.')
|
||||
if (segments.length !== 4) return false
|
||||
|
||||
return segments.every((segment) => {
|
||||
if (!/^\d+$/.test(segment)) return false
|
||||
const value = Number.parseInt(segment, 10)
|
||||
return value >= 0 && value <= 255
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalization converts addresses to a canonical form (lowercase-host:port, default port 25565)
|
||||
*/
|
||||
export function normalizeServerAddress(address: string): string {
|
||||
const trimmedAddress = address.trim()
|
||||
const host = parseServerHost(trimmedAddress)
|
||||
if (!host) return ''
|
||||
let port = DEFAULT_MINECRAFT_SERVER_PORT
|
||||
|
||||
// ipv6 address
|
||||
if (trimmedAddress.startsWith('[')) {
|
||||
const closingBracket = trimmedAddress.indexOf(']')
|
||||
if (closingBracket > 0) {
|
||||
const suffix = trimmedAddress.slice(closingBracket + 1)
|
||||
if (suffix.startsWith(':')) {
|
||||
const parsedPort = parseServerPort(suffix.slice(1))
|
||||
if (parsedPort != null) {
|
||||
port = parsedPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ipv4 address or hostname
|
||||
} else {
|
||||
const firstColon = trimmedAddress.indexOf(':')
|
||||
const lastColon = trimmedAddress.lastIndexOf(':')
|
||||
if (firstColon !== -1 && firstColon === lastColon) {
|
||||
const parsedPort = parseServerPort(trimmedAddress.slice(firstColon + 1))
|
||||
if (parsedPort != null) {
|
||||
port = parsedPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${host}:${port}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain key used for deduping server entries by removing a single leading subdomain.
|
||||
* Example: test.cobblemon.gg and cobblemon.gg map to cobblemon.gg
|
||||
*/
|
||||
export function getServerDomainKey(address: string): string {
|
||||
const normalizedAddress = normalizeServerAddress(address)
|
||||
if (!normalizedAddress) return ''
|
||||
|
||||
const separator = normalizedAddress.lastIndexOf(':')
|
||||
if (separator <= 0 || separator === normalizedAddress.length - 1) return normalizedAddress
|
||||
|
||||
const host = normalizedAddress.slice(0, separator).replace(/\.+$/, '')
|
||||
if (!host) return normalizedAddress
|
||||
if (host.includes(':') || isIPv4Host(host)) return normalizedAddress
|
||||
|
||||
const segments = host.split('.').filter(Boolean)
|
||||
if (segments.length <= 2) return host
|
||||
|
||||
return segments.slice(1).join('.')
|
||||
}
|
||||
|
||||
export function resolveManagedServerWorld(
|
||||
worlds: World[],
|
||||
managedName: string | null | undefined,
|
||||
managedAddress: string | null | undefined,
|
||||
): ServerWorld | null {
|
||||
if (!managedName || !managedAddress) return null
|
||||
|
||||
const normalizedManagedAddress = normalizeServerAddress(managedAddress)
|
||||
if (!normalizedManagedAddress) return null
|
||||
|
||||
const servers = worlds
|
||||
.filter(isServerWorld)
|
||||
.slice()
|
||||
.sort((a, b) => a.index - b.index)
|
||||
|
||||
const exactMatch = servers.find(
|
||||
(server) =>
|
||||
server.name === managedName &&
|
||||
normalizeServerAddress(server.address) === normalizedManagedAddress,
|
||||
)
|
||||
if (exactMatch) return exactMatch
|
||||
|
||||
return (
|
||||
servers.find((server) => normalizeServerAddress(server.address) === normalizedManagedAddress) ??
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerLatency(
|
||||
address: string,
|
||||
protocolVersion: ProtocolVersion | null = null,
|
||||
): Promise<number | undefined> {
|
||||
const pings: number[] = []
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
const status = await get_server_status(address, protocolVersion)
|
||||
if (status.ping != null) {
|
||||
pings.push(status.ping)
|
||||
}
|
||||
} catch {
|
||||
// Ignore individual ping failures
|
||||
}
|
||||
}
|
||||
if (pings.length === 0) return undefined
|
||||
return Math.round(pings.reduce((sum, p) => sum + p, 0) / pings.length)
|
||||
}
|
||||
|
||||
export async function refreshServerData(
|
||||
serverData: ServerData,
|
||||
protocolVersion: ProtocolVersion | null,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { buildLocaleMessages, createMessageCompiler, type CrowdinMessages } from '@modrinth/ui'
|
||||
import { uiLocaleModulesEager } from '@modrinth/ui/src/locales.eager.ts'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const localeModules = import.meta.glob<{ default: CrowdinMessages }>('./locales/*/index.json', {
|
||||
eager: true,
|
||||
})
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en-US',
|
||||
fallbackLocale: 'en-US',
|
||||
messageCompiler: createMessageCompiler(),
|
||||
missingWarn: false,
|
||||
fallbackWarn: false,
|
||||
messages: buildLocaleMessages(localeModules, uiLocaleModulesEager),
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,305 +0,0 @@
|
||||
{
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Ontwikkelaarmodus geaktiveer."
|
||||
},
|
||||
"app.settings.downloading": {
|
||||
"message": "Aflaai v{version}"
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Voorkoms"
|
||||
},
|
||||
"app.settings.tabs.default-instance-options": {
|
||||
"message": "Verstek instansie opsies"
|
||||
},
|
||||
"app.settings.tabs.feature-flags": {
|
||||
"message": "Kenmerk vlae"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Java installasies"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Privaatheid"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Hulpbronbestuur"
|
||||
},
|
||||
"app.update-toast.body": {
|
||||
"message": "Modrinth App v{version} is gereed om te installeer! Herlaai nou werk, of outomaties wanneer jy naby Modrinth App."
|
||||
},
|
||||
"app.update-toast.body.download-complete": {
|
||||
"message": "Modrinth App v{version} klaar afgelaai. Herlaai nou werk, of outomaties wanneer jy naby Modrinth App."
|
||||
},
|
||||
"app.update-toast.body.metered": {
|
||||
"message": "Modrinth App v{version} is nou beskikbaar! Aangesien jy op'n gemeterde netwerk, ons het nie outomaties laai dit."
|
||||
},
|
||||
"app.update-toast.changelog": {
|
||||
"message": "Alle veranderinge"
|
||||
},
|
||||
"app.update-toast.download": {
|
||||
"message": "Laai ({size})"
|
||||
},
|
||||
"app.update-toast.downloading": {
|
||||
"message": "Aflaai..."
|
||||
},
|
||||
"app.update-toast.reload": {
|
||||
"message": "Herlaai"
|
||||
},
|
||||
"app.update-toast.title": {
|
||||
"message": "Die opdatering is beskikbaar"
|
||||
},
|
||||
"app.update-toast.title.download-complete": {
|
||||
"message": "Aflaai voltooi"
|
||||
},
|
||||
"app.update.complete-toast.text": {
|
||||
"message": "Klik hier om die veranderinge te sien."
|
||||
},
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Weergawe {version} is suksesvol geïnstalleer!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Laai die opdatering af"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Laai tans opdatering af ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Herlaai om opdatering te installeer"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "voeg by en speel"
|
||||
},
|
||||
"instance.add-server.add-server": {
|
||||
"message": "bediener byvoeg"
|
||||
},
|
||||
"instance.add-server.resource-pack.disabled": {
|
||||
"message": "Gestrem"
|
||||
},
|
||||
"instance.add-server.resource-pack.enabled": {
|
||||
"message": "aangeskakel"
|
||||
},
|
||||
"instance.add-server.resource-pack.prompt": {
|
||||
"message": "spoed"
|
||||
},
|
||||
"instance.add-server.title": {
|
||||
"message": "voeg 'n bediener by"
|
||||
},
|
||||
"instance.edit-server.title": {
|
||||
"message": "wysig bediener"
|
||||
},
|
||||
"instance.edit-world.hide-from-home": {
|
||||
"message": "versteek van die tuisblad"
|
||||
},
|
||||
"instance.edit-world.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
"instance.edit-world.placeholder-name": {
|
||||
"message": "Minecraft Wêreld"
|
||||
},
|
||||
"instance.edit-world.reset-icon": {
|
||||
"message": "herstel ikoon"
|
||||
},
|
||||
"instance.edit-world.title": {
|
||||
"message": "wêreld wysig"
|
||||
},
|
||||
"instance.filter.disabled": {
|
||||
"message": "gestremde projekte"
|
||||
},
|
||||
"instance.filter.updates-available": {
|
||||
"message": "opdaterings beskikbaar"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "adres"
|
||||
},
|
||||
"instance.server-modal.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
"instance.server-modal.placeholder-name": {
|
||||
"message": "Minecraft-bediener"
|
||||
},
|
||||
"instance.server-modal.resource-pack": {
|
||||
"message": "Hulpbronpakket"
|
||||
},
|
||||
"instance.settings.tabs.general": {
|
||||
"message": "Algemeen"
|
||||
},
|
||||
"instance.settings.tabs.general.delete": {
|
||||
"message": "verwyder instansie"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.button": {
|
||||
"message": "verwyder instansie"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Vee 'n instansie permanent van jou toestel uit, insluitend jou wêrelde, konfigurasies en alle geïnstalleerde inhoud. Wees versigtig, want sodra jy 'n instansie verwyder het, is daar geen manier om dit te herstel nie."
|
||||
},
|
||||
"instance.settings.tabs.general.deleting.button": {
|
||||
"message": "Vee tans uit …"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button": {
|
||||
"message": "Duplikaat"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
|
||||
"message": "Kan nie dupliseer tydens installasie nie."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance": {
|
||||
"message": "Duplikaat instansie"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "Skep 'n kopie van hierdie instansie, insluitend wêrelde, konfigurasies, mods, ens."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "Wwysig ikoon"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.remove": {
|
||||
"message": "Verwyder ikoon"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.replace": {
|
||||
"message": "Vervang ikoon"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.select": {
|
||||
"message": "Kies ikoon"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups": {
|
||||
"message": "Biblioteekgroepe"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.create": {
|
||||
"message": "Skep nuwe groep"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.description": {
|
||||
"message": "Biblioteekgroepe laat jou toe om jou instansies in verskillende afdelings in jou biblioteek te organiseer."
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||
"message": "Voer groepnaam in"
|
||||
},
|
||||
"instance.settings.tabs.general.name": {
|
||||
"message": "Naam"
|
||||
},
|
||||
"instance.settings.tabs.hooks": {
|
||||
"message": "Lanseerhake"
|
||||
},
|
||||
"instance.settings.tabs.hooks.custom-hooks": {
|
||||
"message": "Pasgemaakte lanseerhake"
|
||||
},
|
||||
"instance.settings.tabs.hooks.description": {
|
||||
"message": "Hooks laat gevorderde gebruikers toe om sekere stelselopdragte uit te voer voor en na die bekendstelling van die spel."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit": {
|
||||
"message": "Na-uitgang"
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.description": {
|
||||
"message": "Het gehardloop nadat die wedstryd geëindig het."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.enter": {
|
||||
"message": "Voer die na-uitgang-opdrag in..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch": {
|
||||
"message": "Voorbekendstelling"
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.description": {
|
||||
"message": "Het gehardloop voordat die instansie geloods is."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.enter": {
|
||||
"message": "Voer pre-lanceringsopdrag in..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper": {
|
||||
"message": "Verpakking"
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.description": {
|
||||
"message": "Verpakking opdrag vir die bekendstelling Van Minecraft."
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.enter": {
|
||||
"message": "Voer die Verpakking-opdrag in..."
|
||||
},
|
||||
"instance.settings.tabs.installation": {
|
||||
"message": "Installation"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.modded": {
|
||||
"message": "{platform} {version} vir Minecraft {game_version} reeds geïnstalleer"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
|
||||
"message": "Vanilla {game_version} reeds geïnstalleer"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button": {
|
||||
"message": "Verander weergawe"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.install": {
|
||||
"message": "Installeer"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.installing": {
|
||||
"message": "Installasie"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
|
||||
"message": "Haal modpack weergawes"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.in-progress": {
|
||||
"message": "Installeer nuwe weergawe"
|
||||
},
|
||||
"instance.settings.tabs.installation.currently-installed": {
|
||||
"message": "Huidiglik geïnstalleer"
|
||||
},
|
||||
"instance.settings.tabs.installation.debug-information": {
|
||||
"message": "Ontfoutinligting:"
|
||||
},
|
||||
"instance.settings.tabs.installation.fetching-modpack-details": {
|
||||
"message": "Haal modpack besonderhede"
|
||||
},
|
||||
"instance.settings.tabs.installation.game-version": {
|
||||
"message": "Spel weergawe"
|
||||
},
|
||||
"instance.settings.tabs.installation.install": {
|
||||
"message": "Installeer"
|
||||
},
|
||||
"instance.settings.tabs.installation.install.in-progress": {
|
||||
"message": "Installasie aan die gang"
|
||||
},
|
||||
"instance.settings.tabs.installation.loader-version": {
|
||||
"message": "{loader} weergawe"
|
||||
},
|
||||
"instance.settings.tabs.installation.minecraft-version": {
|
||||
"message": "Minecraft {version}"
|
||||
},
|
||||
"instance.settings.tabs.installation.no-connection": {
|
||||
"message": "Kan nie gekoppelde modpack-besonderhede haal nie. Kontroleer asseblief u internetverbinding."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-loader-versions": {
|
||||
"message": "{loader} is nie beskikbaar Vir Minecraft {version} nie. Probeer'n ander mod loader."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-modpack-found": {
|
||||
"message": "Hierdie geval is gekoppel aan'n modpack, maar die modpack kon nie gevind word op Modrinth."
|
||||
},
|
||||
"instance.settings.tabs.installation.platform": {
|
||||
"message": "Basis"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button": {
|
||||
"message": "Herinstalleer modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
|
||||
"message": "Herinstalleer modpack weer"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.description": {
|
||||
"message": "Herinstalleer sal alle geïnstalleerde of gewysigde inhoud terugstel na wat deur die modpack verskaf word, en enige mods of inhoud wat jy bo-op die oorspronklike installasie bygevoeg het, verwyder. Dit kan onverwagte gedrag regstel as veranderinge aan die instansie aangebring is, maar as jou wêrelde nou afhanklik is van addisionele geïnstalleerde inhoud, kan dit bestaande wêrelde breek."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.title": {
|
||||
"message": "Wil u definitief hierdie instansie herinstalleer?"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.description": {
|
||||
"message": "Stel die inhoud van die instansie terug na sy oorspronklike toestand, en verwyder enige mods of inhoud wat u bo-op die oorspronklike modpack bygevoeg het."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.title": {
|
||||
"message": "Herinstalleer modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button": {
|
||||
"message": "Herstel"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button.repairing": {
|
||||
"message": "Herstel"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.description": {
|
||||
"message": "Herstel herinstalleer Minecraft afhanklikhede en tjeks vir korrupsie. Dit kan probleme oplos as u speletjie nie begin nie weens foute wat verband hou met die lanseerder, maar dit sal nie probleme of ineenstortings wat verband hou met geïnstalleerde mods oplos nie."
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.title": {
|
||||
"message": "Herstel geval?"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.in-progress": {
|
||||
"message": "Herstelwerk aan die gang"
|
||||
}
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
{
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "وضع المطوّر مُفعَّل."
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "المظهر"
|
||||
},
|
||||
"app.settings.tabs.default-instance-options": {
|
||||
"message": "خيارات النسخة الافتراضية"
|
||||
},
|
||||
"app.settings.tabs.feature-flags": {
|
||||
"message": "إعدادات المميزات"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "تثبيتات جافا"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "الخصوصية"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "إدارة الموارد"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "إضافة واللعب"
|
||||
},
|
||||
"instance.add-server.add-server": {
|
||||
"message": "إضافة الخادم"
|
||||
},
|
||||
"instance.add-server.resource-pack.disabled": {
|
||||
"message": "معطَّل"
|
||||
},
|
||||
"instance.add-server.resource-pack.enabled": {
|
||||
"message": "مفعَّل"
|
||||
},
|
||||
"instance.add-server.resource-pack.prompt": {
|
||||
"message": "موجه الأوامر"
|
||||
},
|
||||
"instance.add-server.title": {
|
||||
"message": "إضافة خادم"
|
||||
},
|
||||
"instance.edit-server.title": {
|
||||
"message": "تعديل خادم"
|
||||
},
|
||||
"instance.edit-world.hide-from-home": {
|
||||
"message": "إخفاء من الصفحة الرئيسية"
|
||||
},
|
||||
"instance.edit-world.name": {
|
||||
"message": "الاسم"
|
||||
},
|
||||
"instance.edit-world.placeholder-name": {
|
||||
"message": "عالم ماينكرافت"
|
||||
},
|
||||
"instance.edit-world.reset-icon": {
|
||||
"message": "إعادة تعيين الأيقونة"
|
||||
},
|
||||
"instance.edit-world.title": {
|
||||
"message": "تعديل العالم"
|
||||
},
|
||||
"instance.filter.disabled": {
|
||||
"message": "المشاريع المعطَّلة"
|
||||
},
|
||||
"instance.filter.updates-available": {
|
||||
"message": "توجد تحديثات متاحة"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "العنوان"
|
||||
},
|
||||
"instance.server-modal.name": {
|
||||
"message": "الاسم"
|
||||
},
|
||||
"instance.server-modal.placeholder-name": {
|
||||
"message": "خادم ماين كرافت"
|
||||
},
|
||||
"instance.server-modal.resource-pack": {
|
||||
"message": "حزمة الموارد"
|
||||
},
|
||||
"instance.settings.tabs.general": {
|
||||
"message": "عام"
|
||||
},
|
||||
"instance.settings.tabs.general.delete": {
|
||||
"message": "حذف النسخة"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.button": {
|
||||
"message": "حذف النسخة"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "يحذف النسخة نهائيًا من جهازك، بما في ذلك عوالمك، إعداداتك، وجميع المحتويات المثبّتة. كن حذرًا، فبمجرد حذف النسخة لن تكون هناك أي طريقة لاستعادتها."
|
||||
},
|
||||
"instance.settings.tabs.general.deleting.button": {
|
||||
"message": "جاري الحذف..."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button": {
|
||||
"message": "نسخ"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
|
||||
"message": "لا يمكن النسخ أثناء التثبيت."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance": {
|
||||
"message": "نسخة النسخة"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "إنشاء."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "تعديل الأيقونة"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.remove": {
|
||||
"message": "أزل الأيقونة"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.replace": {
|
||||
"message": "استبدل الأيقونة"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.select": {
|
||||
"message": "اختر الأيقونة"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups": {
|
||||
"message": "مجموعات المكتبة"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.create": {
|
||||
"message": "إنشاء مجموعة جديدة"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.description": {
|
||||
"message": "مجموعات المكتبة تساعدك على ترتيب حالاتك على أقسام مختلفة في مكتبتك."
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||
"message": "إدخال اسم المجموعة"
|
||||
},
|
||||
"instance.settings.tabs.general.name": {
|
||||
"message": "الاسم"
|
||||
},
|
||||
"instance.settings.tabs.hooks": {
|
||||
"message": "إجراءات تشغيل إضافية"
|
||||
},
|
||||
"instance.settings.tabs.hooks.custom-hooks": {
|
||||
"message": "إجراءات تشغيل إضافية مخصصة"
|
||||
},
|
||||
"instance.settings.tabs.hooks.description": {
|
||||
"message": "إجراءات التشغيل المضافة تسمح للمستخدمين المتقدمين بإنشاء أوامر نظامية محددة قبل و عند تشغيل اللعبة."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit": {
|
||||
"message": "ما بعد الخروج"
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.description": {
|
||||
"message": "يتم تشغيله بعد إغلاق اللعبة."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.enter": {
|
||||
"message": "أدخل أمر ما بعد الخروج..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch": {
|
||||
"message": "ما قبل الإطلاق"
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.description": {
|
||||
"message": "يتم تشغيله قبل بَدْء تشغيل النسخة."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.enter": {
|
||||
"message": "أدخل أمر ما قبل الإطلاق..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.title": {
|
||||
"message": "خطافات تشغيل اللعبة"
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper": {
|
||||
"message": "مُغلِّف"
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.description": {
|
||||
"message": "أمر غلاف لتشغيل ماين كرافت."
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.enter": {
|
||||
"message": "أدخل أمر الغلاف..."
|
||||
},
|
||||
"instance.settings.tabs.installation": {
|
||||
"message": "تثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.modded": {
|
||||
"message": "{platform} {version} لماين كرافت {game_version} مثبت بالفعل"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
|
||||
"message": "فانيلا {game_version} مُثبّتة بالفعل"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button": {
|
||||
"message": "تغيير الإصدار"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.install": {
|
||||
"message": "تثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.installing": {
|
||||
"message": "جاري التثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
|
||||
"message": "جاري جلب إصدارات حزمة المودات"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.in-progress": {
|
||||
"message": "جاري تثبيت الإصدار الجديد"
|
||||
},
|
||||
"instance.settings.tabs.installation.currently-installed": {
|
||||
"message": "المثبت حاليًا"
|
||||
},
|
||||
"instance.settings.tabs.installation.debug-information": {
|
||||
"message": "معلومات التصحيح:"
|
||||
},
|
||||
"instance.settings.tabs.installation.fetching-modpack-details": {
|
||||
"message": "جاري جلب تفاصيل حزمة المودات"
|
||||
},
|
||||
"instance.settings.tabs.installation.game-version": {
|
||||
"message": "إصدار اللعبة"
|
||||
},
|
||||
"instance.settings.tabs.installation.install": {
|
||||
"message": "تثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.install.in-progress": {
|
||||
"message": "التثبيت قيد التنفيذ"
|
||||
},
|
||||
"instance.settings.tabs.installation.loader-version": {
|
||||
"message": "إصدار {loader}"
|
||||
},
|
||||
"instance.settings.tabs.installation.minecraft-version": {
|
||||
"message": "ماين كرافت {version}"
|
||||
},
|
||||
"instance.settings.tabs.installation.no-connection": {
|
||||
"message": "تعذّر جلب تفاصيل حزمة المودات المرتبطة. يرجى التحقق من اتصالك بالإنترنت."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-loader-versions": {
|
||||
"message": "{loader} غير متاح لماين كرافت {version}. جرّب محمّل مودات آخر."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-modpack-found": {
|
||||
"message": "هذه النسخة مرتبطة بحزمة مودات، لكن لم يتم العثور على الحزمة على مودرنث."
|
||||
},
|
||||
"instance.settings.tabs.installation.platform": {
|
||||
"message": "منصّة"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button": {
|
||||
"message": "إعادة تثبيت حزمة المودات"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
|
||||
"message": "جاري إعادة تثبيت حزمة المودات"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.description": {
|
||||
"message": "إعادة التثبيت ستُعيد جميع الملفات المثبتة أو المعدلة إلى ما توفره حُزْمة المودات، مع إزالة أي مودات أو محتوى أضفته بعد التثبيت الأصلي. قد يساعد ذلك في حل السلوك غير المتوقع إذا تم تعديل النسخة، لكن إذا كانت عوالمك تعتمد على محتويات إضافية، فقد يتسبب ذلك في تعطلها."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.title": {
|
||||
"message": "هل أنت متأكد أنك تريد إعادة تثبيت هذه النسخة؟"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.description": {
|
||||
"message": "يعيد محتوى النسخة إلى حالته الأصلية، مع إزالة جميع المودات أو المحتوى الذي أُضيف فوق حزمة المودات الأصلية."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.title": {
|
||||
"message": "إعادة تثبيت حزمة المودات"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button": {
|
||||
"message": "إصلاح"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button.repairing": {
|
||||
"message": "جاري الإصلاح"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.description": {
|
||||
"message": "الإصلاح يعيد تثبيت مكوّنات ماين كرافت ويتحقق من التلف. قد يساعد ذلك في حل المشكلات إذا كانت لعبتك لا تعمل بسبب أخطاء متعلقة ببرنامج التشغيل، لكنه لن يحل المشكلات أو الأعطال الناتجة عن المودات المثبّتة."
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.title": {
|
||||
"message": "إصلاح النسخة؟"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.in-progress": {
|
||||
"message": "الإصلاح قيد التنفيذ"
|
||||
},
|
||||
"instance.settings.tabs.installation.reset-selections": {
|
||||
"message": "إعادة التعيين إلى الحالة الحالية"
|
||||
},
|
||||
"instance.settings.tabs.installation.show-all-versions": {
|
||||
"message": "عرض جميع الإصدارات"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.change-version": {
|
||||
"message": "تغيير الإصدار"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.install": {
|
||||
"message": "تثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.reinstall": {
|
||||
"message": "إعادة التثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.repair": {
|
||||
"message": "إصلاح"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
|
||||
"message": "لا يمكن {action} أثناء التثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
|
||||
"message": "لا يمكن {action} أثناء عدم الاتصال بالإنترنت"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-repairing": {
|
||||
"message": "لا يمكن {action} أثناء الإصلاح"
|
||||
},
|
||||
"instance.settings.tabs.installation.unknown-version": {
|
||||
"message": "(إصدار غير معروف)"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.button": {
|
||||
"message": "إلغاء ربط النسخة"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.confirm.description": {
|
||||
"message": "إذا تابعت، فلن تتمكّن من إعادة ربطها إلا بإنشاء نسخة جديدة بالكامل. لن تتلقى بعد ذلك تحديثات حزمة المودات، وستصبح نسخة عادية."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.confirm.title": {
|
||||
"message": "هل أنت متأكد أنك تريد إلغاء ربط هذه النسخة؟"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.description": {
|
||||
"message": "هذه النسخة مرتبطة بحزمة مودات، مما يعني أنه لا يمكن تحديث المودات أو تغيير محمّل المودات أو إصدار ماين كرافت. سيؤدي إلغاء الربط إلى فصل هذه النسخة نهائيًا عن حزمة المودات."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.title": {
|
||||
"message": "إلغاء الربط من حزمة المودات"
|
||||
},
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "جافا والذاكرة"
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "المتغيرات البيئية"
|
||||
},
|
||||
"instance.settings.tabs.java.hooks": {
|
||||
"message": "الخطافات"
|
||||
},
|
||||
"instance.settings.tabs.java.java-arguments": {
|
||||
"message": "وسائط جافا"
|
||||
},
|
||||
"instance.settings.tabs.java.java-installation": {
|
||||
"message": "تثبيت جافا"
|
||||
},
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "الذاكرة المخصَّصة"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "النافذة"
|
||||
},
|
||||
"instance.settings.tabs.window.custom-window-settings": {
|
||||
"message": "إعدادات نافذة مخصّصة"
|
||||
},
|
||||
"instance.settings.tabs.window.fullscreen": {
|
||||
"message": "ملء الشاشة"
|
||||
},
|
||||
"instance.settings.tabs.window.fullscreen.description": {
|
||||
"message": "جعل اللعبة تبدأ في وضع ملء الشاشة عند التشغيل (باستخدام options.txt)."
|
||||
},
|
||||
"instance.settings.tabs.window.height": {
|
||||
"message": "الارتفاع"
|
||||
},
|
||||
"instance.settings.tabs.window.height.description": {
|
||||
"message": "ارتفاع نافذة اللعبة عند التشغيل."
|
||||
},
|
||||
"instance.settings.tabs.window.height.enter": {
|
||||
"message": "أدخل الارتفاع..."
|
||||
},
|
||||
"instance.settings.tabs.window.width": {
|
||||
"message": "العرض"
|
||||
},
|
||||
"instance.settings.tabs.window.width.description": {
|
||||
"message": "عرض نافذة اللعبة عند التشغيل."
|
||||
},
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "أدخل العرض..."
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "الإعدادات"
|
||||
},
|
||||
"instance.worlds.a_minecraft_server": {
|
||||
"message": "خادم ماين كرافت"
|
||||
},
|
||||
"instance.worlds.cant_connect": {
|
||||
"message": "لا يمكن الاتصال بالخادم"
|
||||
},
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "نسخ العنوان"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "عدم العرض في الصفحة الرئيسية"
|
||||
},
|
||||
"instance.worlds.filter.available": {
|
||||
"message": "متاح"
|
||||
},
|
||||
"instance.worlds.game_already_open": {
|
||||
"message": "النسخة مفتوحة بالفعل"
|
||||
},
|
||||
"instance.worlds.hardcore": {
|
||||
"message": "وضع الهاردكور"
|
||||
},
|
||||
"instance.worlds.incompatible_server": {
|
||||
"message": "الخادم غير متوافق"
|
||||
},
|
||||
"instance.worlds.no_contact": {
|
||||
"message": "تعذّر الاتصال بالخادم"
|
||||
},
|
||||
"instance.worlds.no_server_quick_play": {
|
||||
"message": "يمكنك الدخول مباشرة إلى الخوادم فقط على ماين كرافت Alpha 1.0.5+"
|
||||
},
|
||||
"instance.worlds.no_singleplayer_quick_play": {
|
||||
"message": "يمكنك الدخول مباشرة إلى عوالم اللعب الفردي فقط على ماين كرافت 1.20+"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "تشغيل النسخة"
|
||||
},
|
||||
"instance.worlds.type.server": {
|
||||
"message": "الخادم"
|
||||
},
|
||||
"instance.worlds.type.singleplayer": {
|
||||
"message": "لعب فردي"
|
||||
},
|
||||
"instance.worlds.view_instance": {
|
||||
"message": "عرض النسخة"
|
||||
},
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "العالم قيد الاستخدام"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "مقدَّم من النسخة"
|
||||
},
|
||||
"search.filter.locked.instance-game-version.title": {
|
||||
"message": "إصدار اللعبة مقدَّم من النسخة"
|
||||
},
|
||||
"search.filter.locked.instance-loader.title": {
|
||||
"message": "المحمّل مقدَّم من النسخة"
|
||||
},
|
||||
"search.filter.locked.instance.sync": {
|
||||
"message": "مزامنة مع النسخة"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,60 @@
|
||||
{
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "تعذر الوصول إلى خوادم المصادقة"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "نزل للعب"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "تثبيت"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": ""
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "حُزْمَة مشاركة"
|
||||
},
|
||||
"app.modal.install-to-play.shared-server-instance": {
|
||||
"message": "حُزْمَة خادم مشاركة"
|
||||
},
|
||||
"app.modal.update-to-play.added-count": {
|
||||
"message": "{count} مضاف"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.added": {
|
||||
"message": "تمت الإضافة"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.removed": {
|
||||
"message": "تمت الإزالة"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.updated": {
|
||||
"message": "تم تحديث"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "حدث للعب"
|
||||
},
|
||||
"app.modal.update-to-play.published-date": {
|
||||
"message": "{date, date, long}"
|
||||
},
|
||||
"app.modal.update-to-play.removed-count": {
|
||||
"message": "{count} مزال"
|
||||
},
|
||||
"app.modal.update-to-play.update-required": {
|
||||
"message": "يلزم التحديث"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "هناك تحديث لازم للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
|
||||
},
|
||||
"app.modal.update-to-play.updated-count": {
|
||||
"message": "{count} حُدِّث"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "وضع المطوّر مُفعَّل."
|
||||
"message": "تم تفعيل وضع المطوّر."
|
||||
},
|
||||
"app.settings.downloading": {
|
||||
"message": "جار تنزيل الإصدار {version}"
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "المظهر"
|
||||
@@ -9,22 +63,103 @@
|
||||
"message": "خيارات النسخة الافتراضية"
|
||||
},
|
||||
"app.settings.tabs.feature-flags": {
|
||||
"message": "إعدادات المميزات"
|
||||
"message": "أعلام الميزات"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "تثبيتات جافا"
|
||||
},
|
||||
"app.settings.tabs.language": {
|
||||
"message": "اللغة"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "الخصوصية"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "إدارة الموارد"
|
||||
},
|
||||
"app.update.complete-toast.text": {
|
||||
"message": "انقر هنا لعرض سجلّ التغييرات."
|
||||
},
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "تم تثبيت الإصدار {version} بنجاح!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "تنزيل التحديث"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "جار تنزيل التحديث ({percent}٪)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "أعد التحميل لتثبيت التحديث"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "إضافة صديق"
|
||||
},
|
||||
"friends.action.view-friend-requests": {
|
||||
"message": "{count} {count, plural, one {طلب صداقة} other {طلبات صداقة}}"
|
||||
},
|
||||
"friends.add-friend.submit": {
|
||||
"message": "إرسال طلب صداقة"
|
||||
},
|
||||
"friends.add-friend.title": {
|
||||
"message": "جار إضافة صديق"
|
||||
},
|
||||
"friends.add-friend.username.description": {
|
||||
"message": "قد يختلف عن اسم المستخدم الخاص بهم في ماينكرافت!"
|
||||
},
|
||||
"friends.add-friend.username.placeholder": {
|
||||
"message": "أدخل اسم مستخدم Modrinth..."
|
||||
},
|
||||
"friends.add-friend.username.title": {
|
||||
"message": "ما اسم مستخدم صديقك في Modrinth؟"
|
||||
},
|
||||
"friends.add-friends-to-share": {
|
||||
"message": "<link>أضف أصدقاء</link> لمعرفة ما الذي يلعبونه!"
|
||||
},
|
||||
"friends.friend.cancel-request": {
|
||||
"message": "إلغاء الطلب"
|
||||
},
|
||||
"friends.friend.remove-friend": {
|
||||
"message": "إزالة صديق"
|
||||
},
|
||||
"friends.friend.request-sent": {
|
||||
"message": "إرسال طلب الصداقة"
|
||||
},
|
||||
"friends.friend.view-profile": {
|
||||
"message": "عرض الملف الشخصي"
|
||||
},
|
||||
"friends.heading": {
|
||||
"message": "أصدقاء"
|
||||
},
|
||||
"friends.heading.active": {
|
||||
"message": "نشط"
|
||||
},
|
||||
"friends.heading.offline": {
|
||||
"message": "غير متصل"
|
||||
},
|
||||
"friends.heading.online": {
|
||||
"message": "متصل"
|
||||
},
|
||||
"friends.heading.pending": {
|
||||
"message": "قيد الانتظار"
|
||||
},
|
||||
"friends.no-friends-match": {
|
||||
"message": "لا يوجد أصدقاء يطابقون ''{query}''"
|
||||
},
|
||||
"friends.search-friends-placeholder": {
|
||||
"message": "ابحث عن الأصدقاء..."
|
||||
},
|
||||
"friends.section.heading": {
|
||||
"message": "{title} - {count}"
|
||||
},
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>سجّل الدخول إلى حساب Modrinth</link> لإضافة الأصدقاء ومعرفة ما الذي يلعبونه!"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "إضافة واللعب"
|
||||
},
|
||||
"instance.add-server.add-server": {
|
||||
"message": "إضافة الخادم"
|
||||
"message": "إضافة خادم"
|
||||
},
|
||||
"instance.add-server.resource-pack.disabled": {
|
||||
"message": "معطَّل"
|
||||
@@ -69,7 +204,7 @@
|
||||
"message": "الاسم"
|
||||
},
|
||||
"instance.server-modal.placeholder-name": {
|
||||
"message": "خادم ماين كرافت"
|
||||
"message": "خادم Minecraft"
|
||||
},
|
||||
"instance.server-modal.resource-pack": {
|
||||
"message": "حزمة الموارد"
|
||||
@@ -162,7 +297,7 @@
|
||||
"message": "مُغلِّف"
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.description": {
|
||||
"message": "أمر غلاف لتشغيل ماين كرافت."
|
||||
"message": "أمر غلاف لتشغيل ماينكرافت."
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.enter": {
|
||||
"message": "أدخل أمر الغلاف..."
|
||||
@@ -171,7 +306,7 @@
|
||||
"message": "تثبيت"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.modded": {
|
||||
"message": "{platform} {version} لماين كرافت {game_version} مثبت بالفعل"
|
||||
"message": "{platform} {version} لـ Minecraft{game_version} مثبت بالفعل"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
|
||||
"message": "فانيلا {game_version} مُثبّتة بالفعل"
|
||||
@@ -213,7 +348,7 @@
|
||||
"message": "إصدار {loader}"
|
||||
},
|
||||
"instance.settings.tabs.installation.minecraft-version": {
|
||||
"message": "ماين كرافت {version}"
|
||||
"message": "Minecraft {version}"
|
||||
},
|
||||
"instance.settings.tabs.installation.no-connection": {
|
||||
"message": "تعذّر جلب تفاصيل حزمة المودات المرتبطة. يرجى التحقق من اتصالك بالإنترنت."
|
||||
@@ -353,9 +488,6 @@
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "أدخل العرض..."
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "الإعدادات"
|
||||
},
|
||||
"instance.worlds.a_minecraft_server": {
|
||||
"message": "خادم ماين كرافت"
|
||||
},
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
{
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Режим за разработчици активиран."
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Външен вид"
|
||||
},
|
||||
"app.settings.tabs.default-instance-options": {
|
||||
"message": "Опции по подразбиране"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Java инсталации"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Поверителност"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Контрол на ресурси"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Добави и играй"
|
||||
},
|
||||
"instance.add-server.add-server": {
|
||||
"message": "Добави сървър"
|
||||
},
|
||||
"instance.add-server.resource-pack.disabled": {
|
||||
"message": "Деактивирано"
|
||||
},
|
||||
"instance.add-server.resource-pack.enabled": {
|
||||
"message": "Активирано"
|
||||
},
|
||||
"instance.add-server.title": {
|
||||
"message": "Добавяне на сървър"
|
||||
},
|
||||
"instance.edit-server.title": {
|
||||
"message": "Редактиране на сървър"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
|
||||
"message": "Намиране на версиите на modpack-овете"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.in-progress": {
|
||||
"message": "Инсталиране на нова версия"
|
||||
},
|
||||
"instance.settings.tabs.installation.currently-installed": {
|
||||
"message": "В момента инсталирано"
|
||||
},
|
||||
"instance.settings.tabs.installation.fetching-modpack-details": {
|
||||
"message": "Намиране на детайли на modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.game-version": {
|
||||
"message": "Версия на играта"
|
||||
},
|
||||
"instance.settings.tabs.installation.install": {
|
||||
"message": "Инсталиране"
|
||||
},
|
||||
"instance.settings.tabs.installation.install.in-progress": {
|
||||
"message": "Инсталира се в момента"
|
||||
},
|
||||
"instance.settings.tabs.installation.loader-version": {
|
||||
"message": "{loader} версия"
|
||||
},
|
||||
"instance.settings.tabs.installation.minecraft-version": {
|
||||
"message": "Minecraft {version}"
|
||||
},
|
||||
"instance.settings.tabs.installation.no-connection": {
|
||||
"message": "Не може да се намери този modpack. Моля проверете интернет връзката си."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-loader-versions": {
|
||||
"message": "{loader} не е наличен за Minecraft версия {version}. Моля пробвайте друг mod loader."
|
||||
},
|
||||
"instance.settings.tabs.installation.platform": {
|
||||
"message": "Платформа"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button": {
|
||||
"message": "Преинсталирай"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.button.reinstalling": {
|
||||
"message": "Преинсталиране"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.title": {
|
||||
"message": "Сигурни ли сте, че искате да преинсталирате това?"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.title": {
|
||||
"message": "Повторно инсталиране на modpack"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button": {
|
||||
"message": "Поправи"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button.repairing": {
|
||||
"message": "Поправяне"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.confirm.title": {
|
||||
"message": "Поправи?"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.in-progress": {
|
||||
"message": "Поправка..."
|
||||
},
|
||||
"instance.settings.tabs.installation.reset-selections": {
|
||||
"message": "Нулирай до в моменташен"
|
||||
},
|
||||
"instance.settings.tabs.installation.show-all-versions": {
|
||||
"message": "Покажи всички версии"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.change-version": {
|
||||
"message": "променяте версията"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.install": {
|
||||
"message": "инсталирате"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.reinstall": {
|
||||
"message": "преинсталирате"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.repair": {
|
||||
"message": "поправяте"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-installing": {
|
||||
"message": "Не можете да {action} докато се инсталира."
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.cannot-while-offline": {
|
||||
"message": "Не можете да {action} докато нямате достъп до Интернет"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Прозорец"
|
||||
},
|
||||
"instance.settings.tabs.window.custom-window-settings": {
|
||||
"message": "Различни настройки на прозорец"
|
||||
},
|
||||
"instance.settings.tabs.window.fullscreen": {
|
||||
"message": "Пълен екран"
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
{
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Mode de desenvolupament activat."
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Aparença"
|
||||
},
|
||||
"app.settings.tabs.default-instance-options": {
|
||||
"message": "Opcions d'instància per defecte"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Instal·lacions de Java"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Privacitat"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Afegir i jugar"
|
||||
},
|
||||
"instance.add-server.add-server": {
|
||||
"message": "Afegir servidor"
|
||||
},
|
||||
"instance.add-server.resource-pack.disabled": {
|
||||
"message": "Desactivat"
|
||||
},
|
||||
"instance.add-server.resource-pack.enabled": {
|
||||
"message": "Activat"
|
||||
},
|
||||
"instance.add-server.resource-pack.prompt": {
|
||||
"message": "Preguntar"
|
||||
},
|
||||
"instance.add-server.title": {
|
||||
"message": "Afegir un servidor"
|
||||
},
|
||||
"instance.edit-server.title": {
|
||||
"message": "Editar servidor"
|
||||
},
|
||||
"instance.edit-world.hide-from-home": {
|
||||
"message": "Amagar de l'inici"
|
||||
},
|
||||
"instance.edit-world.name": {
|
||||
"message": "Nom"
|
||||
},
|
||||
"instance.edit-world.placeholder-name": {
|
||||
"message": "Món de Minecraft"
|
||||
},
|
||||
"instance.edit-world.reset-icon": {
|
||||
"message": "Reinicia l'icona"
|
||||
},
|
||||
"instance.edit-world.title": {
|
||||
"message": "Edita el món"
|
||||
},
|
||||
"instance.filter.disabled": {
|
||||
"message": "Projectes desactivats"
|
||||
},
|
||||
"instance.filter.updates-available": {
|
||||
"message": "Actualitzacions disponibles"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Adreça web"
|
||||
},
|
||||
"instance.server-modal.name": {
|
||||
"message": "Nom"
|
||||
},
|
||||
"instance.server-modal.placeholder-name": {
|
||||
"message": "Servidor de Minecraft"
|
||||
},
|
||||
"instance.server-modal.resource-pack": {
|
||||
"message": "Paquet de recursos"
|
||||
},
|
||||
"instance.settings.tabs.general": {
|
||||
"message": "General"
|
||||
},
|
||||
"instance.settings.tabs.general.delete": {
|
||||
"message": "Suprimeix la instància"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.button": {
|
||||
"message": "Suprimir instància"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Suprimeix permanentment una instància del dispositiu, inclosos els vostres mons, configuracions i tot el contingut instal·lat. Aneu amb compte, ja que un cop suprimiu una instància no hi ha manera de recuperar-la."
|
||||
},
|
||||
"instance.settings.tabs.general.deleting.button": {
|
||||
"message": "S'està suprimint..."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button": {
|
||||
"message": "Duplicar"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
|
||||
"message": "No es pot duplicar durant la instal·lació."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance": {
|
||||
"message": "Duplica la instància"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "Crea una còpia d'aquesta instància, incloent-hi mons, configuracions, mods, etc."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "Edita la icona"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.remove": {
|
||||
"message": "Treu icona"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.replace": {
|
||||
"message": "Substitueix icona"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.select": {
|
||||
"message": "Selecciona icona"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups": {
|
||||
"message": "Grup de llibreries"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.create": {
|
||||
"message": "Crear nou grup"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||
"message": "Introdueix nom del grup"
|
||||
},
|
||||
"instance.settings.tabs.general.name": {
|
||||
"message": "Nom"
|
||||
},
|
||||
"instance.settings.tabs.installation": {
|
||||
"message": "Instal·lació"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button": {
|
||||
"message": "Canviar versió"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.install": {
|
||||
"message": "Instal·lar"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.installing": {
|
||||
"message": "Instal·lant"
|
||||
},
|
||||
"instance.settings.tabs.installation.game-version": {
|
||||
"message": "Versió del joc"
|
||||
},
|
||||
"instance.settings.tabs.installation.minecraft-version": {
|
||||
"message": "Minecraft {version}"
|
||||
},
|
||||
"instance.settings.tabs.installation.platform": {
|
||||
"message": "Plataforma"
|
||||
},
|
||||
"instance.settings.tabs.installation.repair.button": {
|
||||
"message": "Reparar"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.change-version": {
|
||||
"message": "canviar versió"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.repair": {
|
||||
"message": "reparar"
|
||||
},
|
||||
"instance.settings.tabs.installation.unknown-version": {
|
||||
"message": "(versió desconeguda)"
|
||||
},
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java i memòria"
|
||||
},
|
||||
"instance.settings.tabs.window.fullscreen": {
|
||||
"message": "Pantalla completa"
|
||||
},
|
||||
"instance.settings.tabs.window.height": {
|
||||
"message": "Altura"
|
||||
},
|
||||
"instance.settings.tabs.window.width": {
|
||||
"message": "Amplada"
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "Configuració"
|
||||
},
|
||||
"instance.worlds.filter.available": {
|
||||
"message": "Disponible"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "Jugar instància"
|
||||
},
|
||||
"instance.worlds.type.server": {
|
||||
"message": "Servidor"
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
{
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Gipaandar ang paagi sa tigpalambo."
|
||||
},
|
||||
"app.settings.downloading": {
|
||||
"message": "Gakarganug sa v{version}"
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Panagway"
|
||||
},
|
||||
"app.settings.tabs.default-instance-options": {
|
||||
"message": "Mga kapilian sa sukaranan nga pananglitan"
|
||||
},
|
||||
"app.settings.tabs.feature-flags": {
|
||||
"message": "Bandera sa mga bahin"
|
||||
},
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Mga pagtaod sa Java"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Pribasiya"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Pagdumala sa kabtangan"
|
||||
},
|
||||
"app.update-toast.body.download-complete": {
|
||||
"message": "Nahuman ang pagkarganug sa Modrinth App v{version}. Pagkarga kausab aron matuman ang kabag-ohan, o unya nalang sa pagsara sa Modrinth App."
|
||||
},
|
||||
"app.update-toast.body.metered": {
|
||||
"message": "Magamit na karon ang Modrinth App! Wala namo karganugi daan kay inihap man ang imong pum-ot."
|
||||
},
|
||||
"app.update-toast.download": {
|
||||
"message": "Karganugi ({size})"
|
||||
},
|
||||
"app.update-toast.downloading": {
|
||||
"message": "Gakarganug..."
|
||||
},
|
||||
"app.update-toast.title.download-complete": {
|
||||
"message": "Nahuman ang pagkarganug"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Karganugi ang kausaban"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Gakarganug sa kausaban ({percent}%)"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Idugang ug dulaa"
|
||||
},
|
||||
"instance.add-server.add-server": {
|
||||
"message": "Idugang ang tigtagad"
|
||||
},
|
||||
"instance.add-server.resource-pack.disabled": {
|
||||
"message": "Dili motugot"
|
||||
},
|
||||
"instance.add-server.resource-pack.enabled": {
|
||||
"message": "Gitugotan"
|
||||
},
|
||||
"instance.add-server.resource-pack.prompt": {
|
||||
"message": "Magpatugot"
|
||||
},
|
||||
"instance.add-server.title": {
|
||||
"message": "Pagdugang og tigtagad"
|
||||
},
|
||||
"instance.edit-server.title": {
|
||||
"message": "Usba ang tigtagad"
|
||||
},
|
||||
"instance.edit-world.hide-from-home": {
|
||||
"message": "Ayaw ipakita sa panid sa Balay"
|
||||
},
|
||||
"instance.edit-world.name": {
|
||||
"message": "Ngalan"
|
||||
},
|
||||
"instance.edit-world.placeholder-name": {
|
||||
"message": "Minecraft nga Kalibutan"
|
||||
},
|
||||
"instance.edit-world.reset-icon": {
|
||||
"message": "Walaa ang amoy"
|
||||
},
|
||||
"instance.edit-world.title": {
|
||||
"message": "Usba ang kalibutan"
|
||||
},
|
||||
"instance.filter.disabled": {
|
||||
"message": "Di-gagana nga mga proyekto"
|
||||
},
|
||||
"instance.filter.updates-available": {
|
||||
"message": "Naay bag-ong mga kausaban"
|
||||
},
|
||||
"instance.server-modal.name": {
|
||||
"message": "Ngalan"
|
||||
},
|
||||
"instance.server-modal.placeholder-name": {
|
||||
"message": "Minecraft nga Tigtagad"
|
||||
},
|
||||
"instance.server-modal.resource-pack": {
|
||||
"message": "Putos sa kabtangan"
|
||||
},
|
||||
"instance.settings.tabs.general.delete": {
|
||||
"message": "Tangtangi ang pananglitan"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.button": {
|
||||
"message": "Tangtangi ang pananglitan"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Malungtarong matangtang ang pananglitan sa imong himan, apil na ang imong mga kalibutan, paghan-ay, ug tanang gitaod nga sulod. Pag-amping, dili na mabawi kung tangtangon na nimo ang pananglitan."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "Buhatan og kopya kining pananglitan, apil na ang imong mga kalibutan, paghan-ay, kausaban, ug uban pa."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "Usba ang amoy"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.remove": {
|
||||
"message": "Tangtangi ang amoy"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.description": {
|
||||
"message": "Mahimo nga mobalik sa sinugdan ang tanang gitaod o giusab nga sulod sa unsay ihatag sa putos sa kausaban, tangtangon ang mga kausaban o sulod nga imong gidugang apil na ang lintunganay nga putos sa kausaban. Mahimo nga maayo ang mga tuhaw nga batasan kon naay pagbag-o sa pananglitan, apan kon gasalig na ang imong kalibutan sa dinugang nga sulod, mahimo nga madaut ani ang daan nga mga kalibutan."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.description": {
|
||||
"message": "Ibalik ang mga sulod sa pananglitan sa unang kahimtang, tangtangon ang mga kausaban o sulod nga imong gidugang apil na ang lintunganay nga putos sa kausaban."
|
||||
},
|
||||
"instance.settings.tabs.window.custom-window-settings": {
|
||||
"message": "Mga gusto sa pinatuyo nga tamboanan"
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "Mga Gusto"
|
||||
},
|
||||
"instance.worlds.view_instance": {
|
||||
"message": "Tan-awa ang pananglitan"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,55 @@
|
||||
{
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Autořizační servery Minecraftu aktuálně nejsou k dispozici. Zkontrolujte si své připojení k internetu a zkuste to znovu později."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Připojení k autorizačním serverům se nezdařilo"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Nainstaluj ke hraní"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Instalovat"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural,one {#mód} other {#módy}}"
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Sdílená instance"
|
||||
},
|
||||
"app.modal.install-to-play.shared-server-instance": {
|
||||
"message": "Sdílená serverová instance"
|
||||
},
|
||||
"app.modal.update-to-play.added-count": {
|
||||
"message": "{count} přidáno"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.added": {
|
||||
"message": "Přidáno"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.removed": {
|
||||
"message": "Odebráno"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.updated": {
|
||||
"message": "Aktualizováno"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Updatuj ke hraní"
|
||||
},
|
||||
"app.modal.update-to-play.published-date": {
|
||||
"message": "{date, date, long}"
|
||||
},
|
||||
"app.modal.update-to-play.removed-count": {
|
||||
"message": "{count} odstraněno"
|
||||
},
|
||||
"app.modal.update-to-play.update-required": {
|
||||
"message": "Je vyžadována aktualizace"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Je nutná aktualizace ke hraní {name}. Prosím updatuj na nejnovější verzi k spuštění hry."
|
||||
},
|
||||
"app.modal.update-to-play.updated-count": {
|
||||
"message": "{count} aktualizováno"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Vývojářský režim povolen."
|
||||
},
|
||||
@@ -17,39 +68,15 @@
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Instalace Javy"
|
||||
},
|
||||
"app.settings.tabs.language": {
|
||||
"message": "Jazyk"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Soukromí"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Správa zdrojů"
|
||||
},
|
||||
"app.update-toast.body": {
|
||||
"message": "Modrinth App v{version} je připravena k instalaci! Znovu načtěte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
|
||||
},
|
||||
"app.update-toast.body.download-complete": {
|
||||
"message": "Modrinth App v{version} byla dokončena ke stažení. Znovu načtěte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
|
||||
},
|
||||
"app.update-toast.body.metered": {
|
||||
"message": "Modrinth App v{version} je nyní k dispozici! Protože jste v síti s měřením, nebyla stažena automaticky."
|
||||
},
|
||||
"app.update-toast.changelog": {
|
||||
"message": "Seznam změn"
|
||||
},
|
||||
"app.update-toast.download": {
|
||||
"message": "Stáhnout ({size})"
|
||||
},
|
||||
"app.update-toast.downloading": {
|
||||
"message": "Stahování..."
|
||||
},
|
||||
"app.update-toast.reload": {
|
||||
"message": "Načíst znovu"
|
||||
},
|
||||
"app.update-toast.title": {
|
||||
"message": "K dispozici je aktualizace"
|
||||
},
|
||||
"app.update-toast.title.download-complete": {
|
||||
"message": "Stahování dokončeno"
|
||||
},
|
||||
"app.update.complete-toast.text": {
|
||||
"message": "Kliknutím sem zobrazíte seznam změn."
|
||||
},
|
||||
@@ -63,7 +90,70 @@
|
||||
"message": "Stahování aktualizace ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Znovu načíst pro nainstalování aktualizace"
|
||||
"message": "Restartovat aplikaci pro nainstalování aktualizace"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Přidat přítele"
|
||||
},
|
||||
"friends.action.view-friend-requests": {
|
||||
"message": "{count} {count, plural, one {žádost} few {žádosti} other {žádostí}} o přátelství"
|
||||
},
|
||||
"friends.add-friend.submit": {
|
||||
"message": "Poslat žádost o přátelství"
|
||||
},
|
||||
"friends.add-friend.title": {
|
||||
"message": "Přidávání kamaráda"
|
||||
},
|
||||
"friends.add-friend.username.description": {
|
||||
"message": "Může být jiné než jejich Minecraft jméno!"
|
||||
},
|
||||
"friends.add-friend.username.placeholder": {
|
||||
"message": "Zadejte uživatelské jméno Modrinth..."
|
||||
},
|
||||
"friends.add-friend.username.title": {
|
||||
"message": "Jak se váš kamarád jmenuje na Modrinthu?"
|
||||
},
|
||||
"friends.add-friends-to-share": {
|
||||
"message": "<link>Přidejte si přátelé</link> a podívejte se, co hrají!"
|
||||
},
|
||||
"friends.friend.cancel-request": {
|
||||
"message": "Zrušit žádost"
|
||||
},
|
||||
"friends.friend.remove-friend": {
|
||||
"message": "Odebrat přítele"
|
||||
},
|
||||
"friends.friend.request-sent": {
|
||||
"message": "Žádost o přátelství odeslána"
|
||||
},
|
||||
"friends.friend.view-profile": {
|
||||
"message": "Zobrazit profil"
|
||||
},
|
||||
"friends.heading": {
|
||||
"message": "Kamarádi"
|
||||
},
|
||||
"friends.heading.active": {
|
||||
"message": "Aktivní"
|
||||
},
|
||||
"friends.heading.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"friends.heading.online": {
|
||||
"message": "Online"
|
||||
},
|
||||
"friends.heading.pending": {
|
||||
"message": "Čeká na vyřízení"
|
||||
},
|
||||
"friends.no-friends-match": {
|
||||
"message": "Žádní přátelé neodpovídají '{query}''"
|
||||
},
|
||||
"friends.search-friends-placeholder": {
|
||||
"message": "Hledat přátele..."
|
||||
},
|
||||
"friends.section.heading": {
|
||||
"message": "{title} - {count}"
|
||||
},
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Přihlašte se ke svému Modrinth účtu</link>, abyste jsi přidali přátele a uviděli co hrají!"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Přidat a hrát"
|
||||
@@ -78,7 +168,7 @@
|
||||
"message": "Zapnuto"
|
||||
},
|
||||
"instance.add-server.resource-pack.prompt": {
|
||||
"message": "Na dotaz"
|
||||
"message": "Ptát se"
|
||||
},
|
||||
"instance.add-server.title": {
|
||||
"message": "Přidat server"
|
||||
@@ -87,7 +177,7 @@
|
||||
"message": "Upravit server"
|
||||
},
|
||||
"instance.edit-world.hide-from-home": {
|
||||
"message": "Schovat z domovské stránky"
|
||||
"message": "Skrýt z domovské obrazovky"
|
||||
},
|
||||
"instance.edit-world.name": {
|
||||
"message": "Název"
|
||||
@@ -96,16 +186,16 @@
|
||||
"message": "Minecraft svět"
|
||||
},
|
||||
"instance.edit-world.reset-icon": {
|
||||
"message": "Reset ikony"
|
||||
"message": "Resetovat ikonu"
|
||||
},
|
||||
"instance.edit-world.title": {
|
||||
"message": "Upravit svět"
|
||||
},
|
||||
"instance.filter.disabled": {
|
||||
"message": "Vypnout projekty"
|
||||
"message": "Vypnuté projekty"
|
||||
},
|
||||
"instance.filter.updates-available": {
|
||||
"message": "Aktualizace jsou dostupné"
|
||||
"message": "K dispozice jsou aktualizace"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Adresa"
|
||||
@@ -117,19 +207,19 @@
|
||||
"message": "Minecraft Server"
|
||||
},
|
||||
"instance.server-modal.resource-pack": {
|
||||
"message": "Balíček textur"
|
||||
"message": "Balíčky textur"
|
||||
},
|
||||
"instance.settings.tabs.general": {
|
||||
"message": "Obecné"
|
||||
},
|
||||
"instance.settings.tabs.general.delete": {
|
||||
"message": "Smazat instanci"
|
||||
"message": "Odstranit instalaci"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.button": {
|
||||
"message": "Smazat instanci"
|
||||
"message": "Odstranit instalaci"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Trvale smaže instanci z vašeho zařízení, včetně vašich světů, konfigurací a veškerého nainstalovaného obsahu. Buďte opatrní, protože jakmile instanci smažete, není možné ji obnovit."
|
||||
"message": "Trvale smaže instalaci z vašeho zařízení, včetně vašich světů, konfigurací a veškerého nainstalovaného obsahu. Buďte opatrní, protože jakmile instalaci smažete, nebude možné ji obnovit."
|
||||
},
|
||||
"instance.settings.tabs.general.deleting.button": {
|
||||
"message": "Mazání..."
|
||||
@@ -141,34 +231,34 @@
|
||||
"message": "Nelze duplikovat při instalaci."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance": {
|
||||
"message": "Duplikovat instanci"
|
||||
"message": "Duplikovat instalaci"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "Vytvoří kopii této instance, včetně světů, konfigurací, modů atd."
|
||||
"message": "Vytvoří kopii této instalace, včetně světů, konfigurací, modů atd."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "Upravit ikonu"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.remove": {
|
||||
"message": "Icona odstranit"
|
||||
"message": "Odstranit ikonu"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.replace": {
|
||||
"message": "Ikona replikace"
|
||||
"message": "Změnit ikonu"
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon.select": {
|
||||
"message": "Výber ikona"
|
||||
"message": "Vybrat ikonu"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups": {
|
||||
"message": "Skupina knihoven"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.create": {
|
||||
"message": "Vytvoří novou skupinu"
|
||||
"message": "Vytvořit novou skupinu"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.description": {
|
||||
"message": "Skupiny knihoven vám umožňují uspořádat instance do různých sekcí v knihovně."
|
||||
"message": "Skupiny knihoven vám umožňují uspořádat instalace do různých sekcí."
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||
"message": "Zadej název skupiny"
|
||||
"message": "Zadejte název skupiny"
|
||||
},
|
||||
"instance.settings.tabs.general.name": {
|
||||
"message": "Název"
|
||||
@@ -195,7 +285,7 @@
|
||||
"message": "Před spuštěním"
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.description": {
|
||||
"message": "Spouštějí se před spuštěním instance."
|
||||
"message": "Spouštějí se před spuštěním instalace."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.enter": {
|
||||
"message": "Zadejte příkaz k provedení před spuštěním..."
|
||||
@@ -216,28 +306,28 @@
|
||||
"message": "Instalace"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.modded": {
|
||||
"message": "{platform} {version} pro Minecraft {game_version} již nainstalována"
|
||||
"message": "{platform} {version} pro Minecraft {game_version} již je nainstalována"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.already-installed.vanilla": {
|
||||
"message": "Vanilla {game_version} již nainstalována"
|
||||
"message": "Vanilla {game_version} již je nainstalována"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button": {
|
||||
"message": "Změnit instalaci"
|
||||
"message": "Změnit verzi"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.install": {
|
||||
"message": "Instalovat"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.button.installing": {
|
||||
"message": "Instalace"
|
||||
"message": "Instalování"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.cannot-while-fetching": {
|
||||
"message": "Načítání verzí modpacku"
|
||||
},
|
||||
"instance.settings.tabs.installation.change-version.in-progress": {
|
||||
"message": "Instalace nových verzí"
|
||||
"message": "Instalace nové verze"
|
||||
},
|
||||
"instance.settings.tabs.installation.currently-installed": {
|
||||
"message": "Aktuální instalace"
|
||||
"message": "Aktuálně nainstalováno"
|
||||
},
|
||||
"instance.settings.tabs.installation.debug-information": {
|
||||
"message": "Debugové informace:"
|
||||
@@ -252,7 +342,7 @@
|
||||
"message": "Instalovat"
|
||||
},
|
||||
"instance.settings.tabs.installation.install.in-progress": {
|
||||
"message": "Probíhající instalace"
|
||||
"message": "Probíhá instalace"
|
||||
},
|
||||
"instance.settings.tabs.installation.loader-version": {
|
||||
"message": "{loader} verze"
|
||||
@@ -261,13 +351,13 @@
|
||||
"message": "Minecraft {version}"
|
||||
},
|
||||
"instance.settings.tabs.installation.no-connection": {
|
||||
"message": "Nelze načíst podrobnosti o modpacku. Zkontrolujte připojení k internetu."
|
||||
"message": "Nelze načíst podrobnosti o modpacku. Prosím, zkontrolujte své připojení k internetu."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-loader-versions": {
|
||||
"message": "{loader} není dostupný pro Minecraft {version}. Zkuste jiný mod loader."
|
||||
},
|
||||
"instance.settings.tabs.installation.no-modpack-found": {
|
||||
"message": "Tato instalace je připojená k modpacku, ale modpack nebyl najit na Modrinthu."
|
||||
"message": "Tato instalace je připojená k modpacku, který ake nebyl najit na Modrinthu."
|
||||
},
|
||||
"instance.settings.tabs.installation.platform": {
|
||||
"message": "Platforma"
|
||||
@@ -279,7 +369,7 @@
|
||||
"message": "Přeinstalovávání modpacku"
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.description": {
|
||||
"message": "Přeinstalace resetuje veškerý nainstalovaný nebo upravený obsah na to, co poskytuje modpack, a odstraní všechny mody nebo obsah, které jste přidali do původní instalaci. To může opravit neočekávané chyby, ale pokud vaše světy závisejí na nově instalovaném obsahu, možná budou poškozeny."
|
||||
"message": "Přeinstalace resetuje veškerý nainstalovaný nebo upravený obsah kromě toho, co poskytuje modpack, a odstraní všechny módy nebo obsah, které jste přidali do původní instalace. To může opravit neočekávané chyby, ale pokud vaše světy závisejí na doinstalovaném obsahu, možná budou poškozeny."
|
||||
},
|
||||
"instance.settings.tabs.installation.reinstall.confirm.title": {
|
||||
"message": "Opravdu si přejete přeinstalovat tuto instalaci?"
|
||||
@@ -312,7 +402,7 @@
|
||||
"message": "Ukázat všechny verze"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.change-version": {
|
||||
"message": "Změnit verzi"
|
||||
"message": "změnit verzi"
|
||||
},
|
||||
"instance.settings.tabs.installation.tooltip.action.install": {
|
||||
"message": "Instalovat"
|
||||
@@ -398,9 +488,6 @@
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "Zadejte šířku..."
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "Nastavení"
|
||||
},
|
||||
"instance.worlds.a_minecraft_server": {
|
||||
"message": "Minecraft Server"
|
||||
},
|
||||
@@ -432,7 +519,7 @@
|
||||
"message": "Můžeš rovnou skočit na server pouze v Minecraftu Alpha 1.0.5+"
|
||||
},
|
||||
"instance.worlds.no_singleplayer_quick_play": {
|
||||
"message": "Můžeš rovnou skočit do světa jednoho hráče pouze v Minecraftu 1.20+"
|
||||
"message": "Můžeš se rovnou připojit do světa jednoho hráče pouze v Minecraftu 1.20+"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "Hrát instanci"
|
||||
|
||||
@@ -1,7 +1,58 @@
|
||||
{
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft authentication servere kan måske være nede lige nu. Tjek din internet forbindelse og prøv igen senere."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Kan ikke nå autentificeringsservere"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installer for at spille"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installer"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# mod} other {# mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.server-requires-mods": {
|
||||
"message": "Denne server kræver mods for at spille. Tryk på installer for at sætte de krævet filler fra modrinth op, så lancer direkte til serveren."
|
||||
},
|
||||
"app.modal.update-to-play.added-count": {
|
||||
"message": "{count} tilføjet"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.added": {
|
||||
"message": "Tilføjet"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.removed": {
|
||||
"message": "Fjernet"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.updated": {
|
||||
"message": "Opdateret"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Opdater for at spille"
|
||||
},
|
||||
"app.modal.update-to-play.published-date": {
|
||||
"message": "{date, date, long}"
|
||||
},
|
||||
"app.modal.update-to-play.removed-count": {
|
||||
"message": "{count} fjernet"
|
||||
},
|
||||
"app.modal.update-to-play.update-required": {
|
||||
"message": "Opdatering krævet"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
|
||||
},
|
||||
"app.modal.update-to-play.updated-count": {
|
||||
"message": "{count} opdateret"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Udvikler-tilstand aktiveret."
|
||||
},
|
||||
"app.settings.downloading": {
|
||||
"message": "Downloader v{version}"
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Udseende"
|
||||
},
|
||||
@@ -14,12 +65,93 @@
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Javainstallationer"
|
||||
},
|
||||
"app.settings.tabs.language": {
|
||||
"message": "Sprog"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Privatliv"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Ressourcestyring"
|
||||
},
|
||||
"app.update.complete-toast.text": {
|
||||
"message": "Klik her for at vise ændringslog."
|
||||
},
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Version {version} var installeret med succes!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Download opdatering"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Downloader opdatering ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Genindlæs for at installere opdatering"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Tilføj en ven"
|
||||
},
|
||||
"friends.action.view-friend-requests": {
|
||||
"message": "{count} venne{count, plural, one {anmodning} other {anmodninger}}"
|
||||
},
|
||||
"friends.add-friend.submit": {
|
||||
"message": "Send en venneanmodning"
|
||||
},
|
||||
"friends.add-friend.title": {
|
||||
"message": "Tilføjer en ven"
|
||||
},
|
||||
"friends.add-friend.username.description": {
|
||||
"message": "Det er muligvis anderledes end deres Minecraft brugernavn!"
|
||||
},
|
||||
"friends.add-friend.username.placeholder": {
|
||||
"message": "Indskriv Modrinth brugernavn..."
|
||||
},
|
||||
"friends.add-friend.username.title": {
|
||||
"message": "Hvad er din vens Modrinth brugernavn?"
|
||||
},
|
||||
"friends.add-friends-to-share": {
|
||||
"message": "<link>Tilføj venner</link> for at se hvad de spiller!"
|
||||
},
|
||||
"friends.friend.cancel-request": {
|
||||
"message": "Annuller anmodning"
|
||||
},
|
||||
"friends.friend.remove-friend": {
|
||||
"message": "Fjern ven"
|
||||
},
|
||||
"friends.friend.request-sent": {
|
||||
"message": "Venneanmodning sendt"
|
||||
},
|
||||
"friends.friend.view-profile": {
|
||||
"message": "Vis profil"
|
||||
},
|
||||
"friends.heading": {
|
||||
"message": "Venner"
|
||||
},
|
||||
"friends.heading.active": {
|
||||
"message": "Aktiv"
|
||||
},
|
||||
"friends.heading.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"friends.heading.online": {
|
||||
"message": "Online"
|
||||
},
|
||||
"friends.heading.pending": {
|
||||
"message": "Afventer"
|
||||
},
|
||||
"friends.no-friends-match": {
|
||||
"message": "Ingen venner som matcher \"{query}\""
|
||||
},
|
||||
"friends.search-friends-placeholder": {
|
||||
"message": "Søg venner..."
|
||||
},
|
||||
"friends.section.heading": {
|
||||
"message": "{title} - {count}"
|
||||
},
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Log ind på en Modrinth konto</link> for at tilføje venner og se hvad de spiller!"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Tilføj og spil"
|
||||
},
|
||||
@@ -144,7 +276,7 @@
|
||||
"message": "Kørte efter spillet lukkes."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.enter": {
|
||||
"message": "Indskriv post-exit kommando..."
|
||||
"message": "Indtast post-exit kommando..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch": {
|
||||
"message": "Pre-launch"
|
||||
@@ -153,7 +285,7 @@
|
||||
"message": "Kørt før en instance bliver kørt."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch.enter": {
|
||||
"message": "Indskriv pre-launch kommando..."
|
||||
"message": "Indtast pre-launch kommando..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.title": {
|
||||
"message": "Spille lunch hooks"
|
||||
@@ -165,7 +297,7 @@
|
||||
"message": "Wrapper kommando for at køre Minecraft."
|
||||
},
|
||||
"instance.settings.tabs.hooks.wrapper.enter": {
|
||||
"message": "Indskriv wrapper kommando..."
|
||||
"message": "Indtast wrapper kommando..."
|
||||
},
|
||||
"instance.settings.tabs.installation": {
|
||||
"message": "Installation"
|
||||
@@ -342,7 +474,7 @@
|
||||
"message": "Højden af spillevinduet når kørt."
|
||||
},
|
||||
"instance.settings.tabs.window.height.enter": {
|
||||
"message": "Indskriv højde..."
|
||||
"message": "Indtast højde..."
|
||||
},
|
||||
"instance.settings.tabs.window.width": {
|
||||
"message": "Bredde"
|
||||
@@ -351,10 +483,7 @@
|
||||
"message": "Bredden på spille vinduet når kørt."
|
||||
},
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "Indskriv bredde..."
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "Indstillinger"
|
||||
"message": "Indtast bredde..."
|
||||
},
|
||||
"instance.worlds.a_minecraft_server": {
|
||||
"message": "En Minecraft server"
|
||||
|
||||
@@ -1,4 +1,67 @@
|
||||
{
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Die Authentifizierungsserver von Minecraft sind eventuell momentan nicht erreichbar. Überprüfe deine Internetverbindung und versuche es später erneut."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Authentifizierungsserver sind nicht erreichbar"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Inhalte benötigt"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installieren zum Spielen"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installieren"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# Mod} other {# Mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.required-modpack": {
|
||||
"message": "Benötigtes Modpack"
|
||||
},
|
||||
"app.modal.install-to-play.server-requires-mods": {
|
||||
"message": "Dieser Server benötigt Mods zum spielen. Klicke auf Installieren um die nötigen Dateien von Modrinth herunterzuladen und dannach direkt dem Server beizutreten."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Geteilte Instanz"
|
||||
},
|
||||
"app.modal.install-to-play.shared-server-instance": {
|
||||
"message": "Geteilte Server Instanz"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Inhalte ansehen"
|
||||
},
|
||||
"app.modal.update-to-play.added-count": {
|
||||
"message": "{count} hinzuäfüägt"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.added": {
|
||||
"message": "Hinzugefügt"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.removed": {
|
||||
"message": "Entfernt"
|
||||
},
|
||||
"app.modal.update-to-play.diff-type.updated": {
|
||||
"message": "Aktualisiert"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Aktualisieren zum Spielen"
|
||||
},
|
||||
"app.modal.update-to-play.published-date": {
|
||||
"message": "{date, date, long}"
|
||||
},
|
||||
"app.modal.update-to-play.removed-count": {
|
||||
"message": "{count} entfernt"
|
||||
},
|
||||
"app.modal.update-to-play.update-required": {
|
||||
"message": "Aktualisierung benötigt"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Eine aktualisierung zum spielen von {name} ist benötigt. Bitte aktualisiere auf die neuste Version um das Spiel zu starten."
|
||||
},
|
||||
"app.modal.update-to-play.updated-count": {
|
||||
"message": "{count} aktualisiert"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Entwicklermodus aktiviert."
|
||||
},
|
||||
@@ -17,39 +80,42 @@
|
||||
"app.settings.tabs.java-installations": {
|
||||
"message": "Java Installationen"
|
||||
},
|
||||
"app.settings.tabs.language": {
|
||||
"message": "Sprache"
|
||||
},
|
||||
"app.settings.tabs.privacy": {
|
||||
"message": "Datenschutz"
|
||||
},
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Ressourcenmanagement"
|
||||
},
|
||||
"app.update-toast.body": {
|
||||
"message": "Modrinth App v{version} ist bereit zur Installation! Lade die App neu, oder schliesse sie, um zu aktualisieren."
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App v{version} ist bereit zur Installation! Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
|
||||
},
|
||||
"app.update-toast.body.download-complete": {
|
||||
"message": "Modrinth App v{version} wurde heruntergeladen. Lade die App neu, oder schliesse sie, um zu aktualisieren."
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} wurde heruntergeladen. Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
|
||||
},
|
||||
"app.update-toast.body.metered": {
|
||||
"app.update-popup.body.linux": {
|
||||
"message": "Modrinth App v{version} ist verfügbar. Benutze deinen Paketmanager zum aktualisieren, um die neusten Features und Bugfixes zu erhalten!"
|
||||
},
|
||||
"app.update-popup.body.metered": {
|
||||
"message": "Modrinth App v{version} ist jetzt verfügbar! Da du in einem begrenzten Netzwerk bist, haben wir es nicht automatisch heruntergeladen."
|
||||
},
|
||||
"app.update-toast.changelog": {
|
||||
"app.update-popup.changelog": {
|
||||
"message": "Änderungen"
|
||||
},
|
||||
"app.update-toast.download": {
|
||||
"app.update-popup.download": {
|
||||
"message": "Herunterladen ({size})"
|
||||
},
|
||||
"app.update-toast.downloading": {
|
||||
"message": "Lade herunter..."
|
||||
"app.update-popup.download-complete": {
|
||||
"message": "Download abgeschlossen"
|
||||
},
|
||||
"app.update-toast.reload": {
|
||||
"app.update-popup.reload": {
|
||||
"message": "Neu Laden"
|
||||
},
|
||||
"app.update-toast.title": {
|
||||
"app.update-popup.title": {
|
||||
"message": "Aktualisierung verfügbar"
|
||||
},
|
||||
"app.update-toast.title.download-complete": {
|
||||
"message": "Download abgeschlossen"
|
||||
},
|
||||
"app.update.complete-toast.text": {
|
||||
"message": "Klicke Hier um die Änderungen zu sehen."
|
||||
},
|
||||
@@ -65,6 +131,69 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Lade neu um Aktualisierung zu installieren"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Freund hinzufügen"
|
||||
},
|
||||
"friends.action.view-friend-requests": {
|
||||
"message": "{count} Freundschaftsanfrage{count, plural, one {} other {n}}"
|
||||
},
|
||||
"friends.add-friend.submit": {
|
||||
"message": "Freundschaftsanfrage senden"
|
||||
},
|
||||
"friends.add-friend.title": {
|
||||
"message": "Einen Freund hinzufügen"
|
||||
},
|
||||
"friends.add-friend.username.description": {
|
||||
"message": "Es könnte anders als ihr Minecraft Nutzername sein!"
|
||||
},
|
||||
"friends.add-friend.username.placeholder": {
|
||||
"message": "Modrinth Nutzernamen eingeben..."
|
||||
},
|
||||
"friends.add-friend.username.title": {
|
||||
"message": "Was ist der Modrinth Nutzername deines Freundes?"
|
||||
},
|
||||
"friends.add-friends-to-share": {
|
||||
"message": "<link>Füge Freunde hinzu</link> um zu sehen, was sie spielen!"
|
||||
},
|
||||
"friends.friend.cancel-request": {
|
||||
"message": "Anfrage abbrechen"
|
||||
},
|
||||
"friends.friend.remove-friend": {
|
||||
"message": "Freund entfernen"
|
||||
},
|
||||
"friends.friend.request-sent": {
|
||||
"message": "Freundschaftsanfrage gesendet"
|
||||
},
|
||||
"friends.friend.view-profile": {
|
||||
"message": "Profil anzeigen"
|
||||
},
|
||||
"friends.heading": {
|
||||
"message": "Freunde"
|
||||
},
|
||||
"friends.heading.active": {
|
||||
"message": "Aktiv"
|
||||
},
|
||||
"friends.heading.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"friends.heading.online": {
|
||||
"message": "Online"
|
||||
},
|
||||
"friends.heading.pending": {
|
||||
"message": "Ausstehend"
|
||||
},
|
||||
"friends.no-friends-match": {
|
||||
"message": "Keine Freunde, die mit \"{query}\" übereinstimmen"
|
||||
},
|
||||
"friends.search-friends-placeholder": {
|
||||
"message": "Freunde suchen..."
|
||||
},
|
||||
"friends.section.heading": {
|
||||
"message": "{title} - {count}"
|
||||
},
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Logge dich in ein Modrinth Konto ein</link> um Freunde hinzuzufügen und zu sehen, was sie spielen!"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Ersteue u starte"
|
||||
},
|
||||
@@ -335,6 +464,15 @@
|
||||
"instance.settings.tabs.installation.unknown-version": {
|
||||
"message": "(unbekannte Version)"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink-server-vanilla.description": {
|
||||
"message": "Diese Instanz ist mit einem Server verknüpft, was bedeutet, dass du die Minecraft Version nicht ändern kannst. Das trennen der Verknüpfung trennt diese Instanz permanent vom Server."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink-server.description": {
|
||||
"message": "Diese Instanz ist mit einem Server verknüpft, was bedeutet, dass du Mods nicht aktualisieren, und den Modloader oder die Minecraft Version nicht ändern kannst. Das trennen der Verknüpfung trennt diese Instanz permanent vom Server."
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink-server.title": {
|
||||
"message": "Serververknüpfung trennen"
|
||||
},
|
||||
"instance.settings.tabs.installation.unlink.button": {
|
||||
"message": "Verlinking der Instanz trennen"
|
||||
},
|
||||
@@ -398,9 +536,6 @@
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "Breite eingeben..."
|
||||
},
|
||||
"instance.settings.title": {
|
||||
"message": "Einstellungen"
|
||||
},
|
||||
"instance.worlds.a_minecraft_server": {
|
||||
"message": "Ä Minecraft-Server"
|
||||
},
|
||||
@@ -425,6 +560,9 @@
|
||||
"instance.worlds.incompatible_server": {
|
||||
"message": "Server ist inkompatibel"
|
||||
},
|
||||
"instance.worlds.linked_server": {
|
||||
"message": "Von Serverprojekt verwaltet"
|
||||
},
|
||||
"instance.worlds.no_contact": {
|
||||
"message": "Server konnte nicht erreicht werden"
|
||||
},
|
||||
@@ -460,5 +598,17 @@
|
||||
},
|
||||
"search.filter.locked.instance.sync": {
|
||||
"message": "Mit Instanz synchronisieren"
|
||||
},
|
||||
"search.filter.locked.server": {
|
||||
"message": "Von Server bereitgestellt"
|
||||
},
|
||||
"search.filter.locked.server-environment.title": {
|
||||
"message": "Nur Clientseitige Mods können der Serverinstanz hinzugefügt werden"
|
||||
},
|
||||
"search.filter.locked.server-game-version.title": {
|
||||
"message": "Spielversion vom Server bereitgestellt"
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader vom Server bereitgestellt"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user