mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0944e88674 | ||
|
|
c84c78bf63 | ||
|
|
19dbcfa000 | ||
|
|
152cb766bf | ||
|
|
ea723f719c | ||
|
|
5abcfe6c38 | ||
|
|
e9dfe1b7f0 | ||
|
|
dfb6814095 | ||
|
|
a80cc7e47b | ||
|
|
fc90e1098e | ||
|
|
747fe04888 | ||
|
|
ab7f649177 | ||
|
|
b39544b50e | ||
|
|
d65e465543 | ||
|
|
ba1d374be6 | ||
|
|
620894aecb | ||
|
|
85ae1f2074 | ||
|
|
3f8fd9cb56 | ||
|
|
a2eed001b2 | ||
|
|
6afda48e70 | ||
|
|
e8be67d41f | ||
|
|
548357c92c | ||
|
|
453369ca07 | ||
|
|
faf593b2af | ||
|
|
e3d6a498d0 | ||
|
|
42cdcc7df9 | ||
|
|
e043a232bc | ||
|
|
c44ead2dbe | ||
|
|
11ac27f71f | ||
|
|
6862cf5ab2 | ||
|
|
16e1bf4611 | ||
|
|
77e4c41480 | ||
|
|
cb93c641d6 | ||
|
|
eebb353547 | ||
|
|
694ab09a01 | ||
|
|
da47c50320 | ||
|
|
bee4391df1 | ||
|
|
d1b122fb21 | ||
|
|
281bf066de | ||
|
|
68fde3ff97 | ||
|
|
7d6d938b99 | ||
|
|
065759d1b8 | ||
|
|
9b3fe6390e | ||
|
|
2236dd8ade | ||
|
|
3a44def301 | ||
|
|
176d4301c3 | ||
|
|
3e32901737 | ||
|
|
ab623dc325 | ||
|
|
bb6e24640c | ||
|
|
15fc6d4e38 | ||
|
|
ed2f04322f | ||
|
|
b9e7b54b4e | ||
|
|
dd51c08a18 | ||
|
|
5244060588 | ||
|
|
9483656881 | ||
|
|
7c642f7078 | ||
|
|
3c2cc7568d | ||
|
|
7b5c746757 | ||
|
|
5b5c8c06e3 | ||
|
|
4d68f3cea4 | ||
|
|
546b117437 | ||
|
|
3d5f29a7a2 | ||
|
|
37b0f7ff98 | ||
|
|
1603796856 | ||
|
|
7c9a9f22d4 | ||
|
|
d38d23dbf3 | ||
|
|
f12bd7b4b8 | ||
|
|
baee34b0b6 | ||
|
|
74bad7456c | ||
|
|
a6f67581d7 | ||
|
|
02be7a8b82 | ||
|
|
d5ad1cb823 | ||
|
|
b666747bc2 | ||
|
|
c1b0e4a692 | ||
|
|
0713814d0c | ||
|
|
0b8b4fb516 | ||
|
|
d6e366b488 | ||
|
|
0682cc3c4f | ||
|
|
693a371d61 | ||
|
|
a2a97d1313 | ||
|
|
71e4f7cb91 | ||
|
|
1a51e58297 | ||
|
|
a9c417d1d1 | ||
|
|
de4f0bffb0 | ||
|
|
e71a8c10fa | ||
|
|
c4b3c6e8d6 | ||
|
|
54c45ac9f3 | ||
|
|
e5f600ddd7 | ||
|
|
4a7525d0a1 | ||
|
|
d0d3aaf09b | ||
|
|
bb3506823d |
@@ -22,4 +22,5 @@ Refer to the standards: @standards/frontend/CROSS_PLATFORM_PAGES.md and @standar
|
||||
- Move the page component into `packages/ui/src/layouts/wrapped/` matching the route structure.
|
||||
- Replace any platform-specific imports with shared utilities.
|
||||
- Import and render the wrapped page from both frontends as a simple component.
|
||||
- If the layout uses TanStack Query for initial route paint with `ReadyTransition` / `useReadyState`, each platform route shell must call `ensureQueryData` for those queries with matching keys and fetchers — see **Platform route shells: prefetch with `ensureQueryData`** in `standards/frontend/CROSS_PLATFORM_PAGES.md`.
|
||||
6. **Verify** the page renders correctly by checking for missing imports and that all DI contracts are satisfied.
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: review-changelog
|
||||
description: Review the latest changelog entry in packages/blog/changelog.ts against the project's changelog style guide and flag bullets that need rewriting. Use when checking a freshly added changelog entry before opening a PR, or when the user asks to review/lint the latest changelog.
|
||||
argument-hint: [product?]
|
||||
---
|
||||
|
||||
Refer to the standard: @standards/maintaining/CHANGELOG.md
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Locate the latest entry:**
|
||||
- Open `packages/blog/changelog.ts`.
|
||||
- The latest entries are at the top of the `VERSIONS` array.
|
||||
- If `$ARGUMENTS` specifies a product (`web`, `hosting`, `app`), review the most recent entry for that product. Otherwise, review the most recent entry overall, plus any sibling entries sharing the same `date` (coordinated releases ship together).
|
||||
|
||||
2. **Read the standard above** in full before reviewing. The bullet rules, section/verb agreement, and "Don't" list are the source of truth.
|
||||
|
||||
3. **Check the entry shell:**
|
||||
- `date` is a valid ISO 8601 timestamp.
|
||||
- `product` is one of `web`, `hosting`, `app`.
|
||||
- `version` is present for `app` entries and omitted for `web`/`hosting`.
|
||||
- Section headings use `## Added`, `## Changed`, `## Fixed`, `## Security` (or a featured-release linked heading). Flag legacy `## Improvements`.
|
||||
|
||||
4. **Review each bullet** against the standard. For each bullet, check:
|
||||
- Voice/tense matches the section heading.
|
||||
- Opening verb agrees with its section.
|
||||
- Describes observable behavior, not implementation.
|
||||
- Specific enough to identify the surface (names the tab/page/modal).
|
||||
- One sentence, ends with a period, sentence case.
|
||||
- Uses branded names (Modrinth App, Modrinth Hosting) correctly.
|
||||
- No filler ("issue with", "issue where", "various", "some"), no vague intensifiers, no apologies, no PR/commit references (unless crediting a third-party contributor with a linked GitHub profile).
|
||||
- Not a duplicate sub-fix of a bigger change already listed.
|
||||
|
||||
5. **Report findings** as a short list grouped by entry. For each problem bullet, show the original line and a suggested rewrite. If the entry is clean, say so explicitly. Do not edit the file unless the user asks - this skill is a review pass, not a rewrite pass.
|
||||
|
||||
6. **If the user then asks to apply fixes**, edit `packages/blog/changelog.ts` directly using the suggested rewrites. Preserve tab indentation and template literal formatting.
|
||||
@@ -27,12 +27,15 @@ on:
|
||||
options:
|
||||
- prod
|
||||
- staging
|
||||
- prod-with-staging-archon
|
||||
default: prod
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
env:
|
||||
VITE_STRIPE_PUBLISHABLE_KEY: pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -9,6 +9,7 @@ jobs:
|
||||
name: Release Modrinth App
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
startsWith(github.event.workflow_run.head_branch, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -23,6 +24,25 @@ jobs:
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🔒 Verify ref is a tag
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
tag_sha=$(gh api "repos/${{ github.repository }}/git/refs/tags/${VERSION_TAG}" --jq '.object.sha' 2>/dev/null || true)
|
||||
if [ -z "$tag_sha" ]; then
|
||||
echo "::error::${VERSION_TAG} is not a git tag in this repo"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$tag_sha" != "$HEAD_SHA" ]; then
|
||||
commit_sha=$(gh api "repos/${{ github.repository }}/git/tags/${tag_sha}" --jq '.object.sha' 2>/dev/null || echo "$tag_sha")
|
||||
if [ "$commit_sha" != "$HEAD_SHA" ]; then
|
||||
echo "::error::Tag ${VERSION_TAG} (${commit_sha}) does not match build head_sha (${HEAD_SHA})"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Verified ${VERSION_TAG} is a tag pointing at ${HEAD_SHA}"
|
||||
|
||||
- name: 📥 Download Modrinth App artifacts
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
@@ -32,12 +52,17 @@ jobs:
|
||||
branch: ${{ env.VERSION_TAG }}
|
||||
use_unzip: true
|
||||
|
||||
- name: 📝 Extract app changelog
|
||||
env:
|
||||
VERSION: ${{ env.VERSION_TAG }}
|
||||
run: npx --yes tsx scripts/build-theseus-release-notes.ts
|
||||
|
||||
- name: 🛠️ Generate version manifest
|
||||
run: |
|
||||
# Reference: https://tauri.app/plugin/updater/#server-support
|
||||
jq -nc \
|
||||
--arg versionTag "${VERSION_TAG#v}" \
|
||||
--arg releaseNotes "See the full changelog at https://modrinth.com/news/changelog" \
|
||||
--rawfile releaseNotes release-notes.md \
|
||||
--rawfile macOsAarch64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
|
||||
--rawfile macOsX64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
|
||||
--rawfile linuxX64UpdateArtifactSignature "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION_TAG#v}_amd64.AppImage.tar.gz.sig" \
|
||||
@@ -120,7 +145,7 @@ jobs:
|
||||
|
||||
gh release create "$VERSION_TAG" \
|
||||
--title "Modrinth App ${VERSION}" \
|
||||
--notes "See the full changelog at https://modrinth.com/news/changelog" \
|
||||
--notes-file release-notes.md \
|
||||
"${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/nsis/Modrinth App_${VERSION}_x64-setup.exe" \
|
||||
"${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/dmg/Modrinth App_${VERSION}_universal.dmg" \
|
||||
"${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION}_amd64.AppImage" \
|
||||
|
||||
@@ -78,3 +78,11 @@ storybook-static
|
||||
|
||||
# frontend robots.txt
|
||||
apps/frontend/src/public/robots.txt
|
||||
|
||||
# Oh My Code
|
||||
.omc/
|
||||
|
||||
# Local dry-run output for scripts/build-theseus-release-notes.mjs
|
||||
/test_result.md
|
||||
|
||||
packages/tooling-config/script-utils/import-sort.js
|
||||
|
||||
Vendored
+3
-1
@@ -29,5 +29,7 @@
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
}
|
||||
},
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"scss.lint.unknownAtRules": "ignore"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
See [CLAUDE.md](./CLAUDE.md) for all project instructions and guidelines.
|
||||
Generated
+31
-79
@@ -27,7 +27,7 @@ checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
|
||||
dependencies = [
|
||||
"actix-utils",
|
||||
"actix-web",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"futures-util",
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -46,7 +46,7 @@ dependencies = [
|
||||
"actix-web",
|
||||
"bitflags 2.9.4",
|
||||
"bytes",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"futures-core",
|
||||
"http-range",
|
||||
"log",
|
||||
@@ -72,7 +72,7 @@ dependencies = [
|
||||
"brotli",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"foldhash",
|
||||
@@ -226,7 +226,7 @@ dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"cookie 0.16.2",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"foldhash",
|
||||
"futures-core",
|
||||
@@ -1902,6 +1902,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
@@ -2450,21 +2459,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.0.1"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.0.1"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
dependencies = [
|
||||
"convert_case 0.10.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.106",
|
||||
"unicode-xid",
|
||||
]
|
||||
@@ -4914,7 +4925,7 @@ dependencies = [
|
||||
"const_format",
|
||||
"dashmap",
|
||||
"deadpool-redis",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"dotenv-build",
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -4941,6 +4952,7 @@ dependencies = [
|
||||
"paste",
|
||||
"path-util",
|
||||
"prometheus",
|
||||
"quick-xml 0.38.3",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"redis",
|
||||
@@ -4973,7 +4985,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"utoipa-actix-web",
|
||||
"utoipa-swagger-ui",
|
||||
"utoipa-scalar",
|
||||
"uuid 1.18.1",
|
||||
"validator",
|
||||
"webp",
|
||||
@@ -5515,7 +5527,7 @@ name = "modrinth-util"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"dotenvy",
|
||||
"eyre",
|
||||
"modrinth-log",
|
||||
@@ -5581,7 +5593,7 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"reqwest 0.12.24",
|
||||
"rust_decimal",
|
||||
"rust_iso3166",
|
||||
@@ -6592,7 +6604,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
name = "path-util"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"itertools 0.14.0",
|
||||
"serde",
|
||||
"typed-path",
|
||||
@@ -7950,40 +7962,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "8.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"syn 2.0.106",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "8.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185"
|
||||
dependencies = [
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
@@ -9308,7 +9286,7 @@ dependencies = [
|
||||
name = "sqlx-tracing"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"futures",
|
||||
"opentelemetry",
|
||||
"opentelemetry-testing",
|
||||
@@ -10181,7 +10159,7 @@ dependencies = [
|
||||
"daedalus",
|
||||
"dashmap",
|
||||
"data-url",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"dirs",
|
||||
"discord-rich-presence",
|
||||
"dotenvy",
|
||||
@@ -10189,6 +10167,7 @@ dependencies = [
|
||||
"either",
|
||||
"encoding_rs",
|
||||
"enumset",
|
||||
"eyre",
|
||||
"flate2",
|
||||
"fs4",
|
||||
"futures",
|
||||
@@ -11199,30 +11178,17 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "9.0.2"
|
||||
name = "utoipa-scalar"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
|
||||
checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"base64 0.22.1",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui-vendored",
|
||||
"zip 3.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui-vendored"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
@@ -12607,20 +12573,6 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap 2.11.4",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ darling = { version = "0.23" }
|
||||
dashmap = "6.1.0"
|
||||
data-url = "0.3.2"
|
||||
deadpool-redis = { git = "https://github.com/modrinth/deadpool", rev = "db5fb00b036ecc8fe5f18853c559b745ffe47bde", version = "0.22.1" }
|
||||
derive_more = "2.0.1"
|
||||
derive_more = "2.1.1"
|
||||
directories = "6.0.0"
|
||||
dirs = "6.0.0"
|
||||
discord-rich-presence = "1.0.0"
|
||||
@@ -208,7 +208,7 @@ url = "2.5.7"
|
||||
urlencoding = "2.1.3"
|
||||
utoipa = { version = "5.4.0", features = ["actix_extras", "chrono", "decimal"] }
|
||||
utoipa-actix-web = { version = "0.1.2" }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["actix-web", "vendored"] }
|
||||
utoipa-scalar = { version = "0.3.0", default-features = false }
|
||||
uuid = "1.18.1"
|
||||
validator = "0.20.0"
|
||||
webp = { version = "0.3.1", default-features = false }
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"test": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@intercom/messenger-js-sdk": "^0.0.14",
|
||||
"@modrinth/api-client": "workspace:^",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
@@ -35,6 +36,7 @@
|
||||
"fuse.js": "^6.6.2",
|
||||
"intl-messageformat": "^10.7.7",
|
||||
"ofetch": "^1.3.4",
|
||||
"overlayscrollbars": "^2.15.1",
|
||||
"pinia": "^3.0.0",
|
||||
"posthog-js": "^1.158.2",
|
||||
"three": "^0.172.0",
|
||||
|
||||
+290
-127
@@ -1,5 +1,13 @@
|
||||
<script setup>
|
||||
import { AuthFeature, PanelVersionFeature, TauriModrinthClient } from '@modrinth/api-client'
|
||||
import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk'
|
||||
import {
|
||||
AuthFeature,
|
||||
NodeAuthFeature,
|
||||
nodeAuthState,
|
||||
PanelVersionFeature,
|
||||
TauriModrinthClient,
|
||||
VerboseLoggingFeature,
|
||||
} from '@modrinth/api-client'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChangeSkinIcon,
|
||||
@@ -11,15 +19,12 @@ import {
|
||||
LibraryIcon,
|
||||
LogInIcon,
|
||||
LogOutIcon,
|
||||
MaximizeIcon,
|
||||
MinimizeIcon,
|
||||
NewspaperIcon,
|
||||
NotepadTextIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
RestoreIcon,
|
||||
RightArrowIcon,
|
||||
ServerIcon,
|
||||
ServerStackIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
WorldIcon,
|
||||
@@ -28,13 +33,13 @@ import {
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
Button,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
ContentInstallModal,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
I18nDebugPanel,
|
||||
LoadingBar,
|
||||
NewsArticleCard,
|
||||
NotificationPanel,
|
||||
OverflowMenu,
|
||||
@@ -49,10 +54,11 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatBytes, renderString } from '@modrinth/utils'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { useQuery, useQueryClient } 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 { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
@@ -61,13 +67,14 @@ import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import AppActionBar from '@/components/ui/AppActionBar.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 UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.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'
|
||||
@@ -77,9 +84,10 @@ 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 WindowControls from '@/components/ui/WindowControls.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import { config } from '@/config'
|
||||
import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable } from '@/helpers/auth.js'
|
||||
@@ -107,8 +115,9 @@ import {
|
||||
import { createServerInstall, provideServerInstall } from '@/providers/server-install'
|
||||
import { setupProviders } from '@/providers/setup'
|
||||
import { setupAuthProvider } from '@/providers/setup/auth'
|
||||
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
|
||||
import { useError } from '@/store/error.js'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
@@ -127,17 +136,34 @@ const { addPopupNotification } = popupNotificationManager
|
||||
|
||||
const tauriApiClient = new TauriModrinthClient({
|
||||
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
|
||||
labrinthBaseUrl: config.labrinthBaseUrl,
|
||||
archonBaseUrl: config.archonBaseUrl,
|
||||
features: [
|
||||
new NodeAuthFeature({
|
||||
getAuth: () => nodeAuthState.getAuth?.() ?? null,
|
||||
refreshAuth: async () => {
|
||||
if (nodeAuthState.refreshAuth) {
|
||||
await nodeAuthState.refreshAuth()
|
||||
}
|
||||
},
|
||||
}),
|
||||
new AuthFeature({
|
||||
token: async () => (await getCreds()).session,
|
||||
token: async () => (await getCreds())?.session,
|
||||
}),
|
||||
new PanelVersionFeature(),
|
||||
new VerboseLoggingFeature(),
|
||||
],
|
||||
})
|
||||
provideModrinthClient(tauriApiClient)
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(true),
|
||||
showAds: ref(false),
|
||||
featureFlags: {
|
||||
serverRamAsBytesAlwaysOn: computed(() =>
|
||||
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
|
||||
),
|
||||
},
|
||||
openExternalUrl: (url) => openUrl(url),
|
||||
})
|
||||
provideModalBehavior({
|
||||
noblur: computed(() => !themeStore.advancedRendering),
|
||||
@@ -147,15 +173,17 @@ provideModalBehavior({
|
||||
|
||||
const {
|
||||
installationModal,
|
||||
unknownPackWarningModal,
|
||||
fetchExistingInstanceNames,
|
||||
handleCreate,
|
||||
handleBrowseModpacks,
|
||||
searchModpacks,
|
||||
getProjectVersions,
|
||||
getLoaderManifest,
|
||||
setModpackAlreadyInstalledModal,
|
||||
handleModpackDuplicateCreateAnyway,
|
||||
handleModpackDuplicateGoToInstance,
|
||||
} = setupProviders(notificationManager)
|
||||
} = setupProviders(notificationManager, popupNotificationManager)
|
||||
|
||||
const news = ref([])
|
||||
const availableSurvey = ref(false)
|
||||
@@ -213,6 +241,7 @@ onMounted(async () => {
|
||||
onUnmounted(async () => {
|
||||
document.querySelector('body').removeEventListener('click', handleClick)
|
||||
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
|
||||
shutdownHostingIntercom()
|
||||
|
||||
await unlistenUpdateDownload?.()
|
||||
})
|
||||
@@ -395,17 +424,112 @@ const handleClose = async () => {
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = setupLoadingStateProvider()
|
||||
loading.setEnabled(false)
|
||||
let initialLoadToken = loading.begin()
|
||||
let routerToken = null
|
||||
let suspenseToken = null
|
||||
|
||||
let suspensePending = false
|
||||
|
||||
const sidebarOverlayScrollbarsOptions = Object.freeze({
|
||||
overflow: {
|
||||
x: 'hidden',
|
||||
y: 'scroll',
|
||||
},
|
||||
})
|
||||
|
||||
router.beforeEach(() => {
|
||||
suspensePending = false
|
||||
if (routerToken) loading.end(routerToken)
|
||||
routerToken = loading.begin()
|
||||
})
|
||||
router.afterEach((to, from, failure) => {
|
||||
trackEvent('PageView', {
|
||||
path: to.path,
|
||||
fromPath: from.path,
|
||||
failed: failure,
|
||||
})
|
||||
setTimeout(() => {
|
||||
if (!suspensePending && stateInitialized.value) {
|
||||
if (initialLoadToken) {
|
||||
loading.end(initialLoadToken)
|
||||
initialLoadToken = null
|
||||
}
|
||||
if (routerToken) {
|
||||
loading.end(routerToken)
|
||||
routerToken = null
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
const route = useRoute()
|
||||
|
||||
const loading = useLoading()
|
||||
loading.setEnabled(false)
|
||||
function onSuspensePending() {
|
||||
suspensePending = true
|
||||
if (suspenseToken) loading.end(suspenseToken)
|
||||
suspenseToken = loading.begin()
|
||||
}
|
||||
|
||||
function onSuspenseResolve() {
|
||||
if (suspenseToken) {
|
||||
loading.end(suspenseToken)
|
||||
suspenseToken = null
|
||||
}
|
||||
if (routerToken) {
|
||||
loading.end(routerToken)
|
||||
routerToken = null
|
||||
}
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
watch(stateInitialized, (ready) => {
|
||||
if (ready) {
|
||||
if (initialLoadToken) {
|
||||
loading.end(initialLoadToken)
|
||||
initialLoadToken = null
|
||||
}
|
||||
if (routerToken) {
|
||||
loading.end(routerToken)
|
||||
routerToken = null
|
||||
}
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['servers'],
|
||||
queryFn: async () => {
|
||||
const response = await tauriApiClient.archon.servers_v0.list({ limit: 100 })
|
||||
const hasMedalServers = response.servers.some((s) => s.is_medal)
|
||||
if (hasMedalServers) {
|
||||
const subscriptions = await tauriApiClient.labrinth.billing_internal.getSubscriptions()
|
||||
for (const server of response.servers) {
|
||||
if (server.is_medal) {
|
||||
const sub = subscriptions.find((s) => s.metadata?.id === server.server_id)
|
||||
if (sub) {
|
||||
server.medal_expires = new Date(
|
||||
new Date(sub.created).getTime() + 5 * 86400000,
|
||||
).toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return response
|
||||
},
|
||||
staleTime: 30_000,
|
||||
})
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['billing', 'subscriptions'],
|
||||
queryFn: () => tauriApiClient.labrinth.billing_internal.getSubscriptions(),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['billing', 'payments'],
|
||||
queryFn: () => tauriApiClient.labrinth.billing_internal.getPayments(),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const error = useError()
|
||||
const errorModal = ref()
|
||||
@@ -459,9 +583,27 @@ setupAuthProvider(credentials, async (_redirectPath) => {
|
||||
await signIn()
|
||||
})
|
||||
|
||||
async function validateSession(sessionToken) {
|
||||
try {
|
||||
const response = await tauriFetch(`${config.labrinthBaseUrl}/v2/user`, {
|
||||
method: 'GET',
|
||||
headers: { Authorization: sessionToken },
|
||||
})
|
||||
if (response.status === 401) return false
|
||||
return true
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCredentials() {
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (creds && creds.user_id) {
|
||||
if (creds.session && !(await validateSession(creds.session))) {
|
||||
await logout().catch(handleError)
|
||||
credentials.value = null
|
||||
return
|
||||
}
|
||||
creds.user = await get_user(creds.user_id, 'bypass').catch(handleError)
|
||||
}
|
||||
credentials.value = creds ?? null
|
||||
@@ -514,6 +656,102 @@ const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value
|
||||
const showAd = computed(
|
||||
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
|
||||
)
|
||||
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
|
||||
const INTERCOM_DEFAULT_PADDING = 20
|
||||
const INTERCOM_APP_SIDEBAR_WIDTH = 300
|
||||
|
||||
let intercomBooting = false
|
||||
let intercomBooted = false
|
||||
|
||||
async function fetchIntercomToken() {
|
||||
const creds = await getCreds()
|
||||
if (!creds?.session) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
const params = new URLSearchParams()
|
||||
if (route.path.startsWith('/hosting/manage/') && typeof route.params.id === 'string') {
|
||||
params.set('server_id', route.params.id)
|
||||
}
|
||||
const query = params.size > 0 ? `?${params.toString()}` : ''
|
||||
|
||||
const response = await tauriFetch(`${config.siteUrl}/api/intercom/messenger-jwt${query}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${creds.session}`,
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch Intercom token: ${response.status}`)
|
||||
}
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function bootIntercom() {
|
||||
if (
|
||||
intercomBooting ||
|
||||
intercomBooted ||
|
||||
!hostingRouteActive.value ||
|
||||
!credentials.value?.session
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
intercomBooting = true
|
||||
console.debug('[APP][INTERCOM] initializing secure support chat')
|
||||
try {
|
||||
const { token } = await fetchIntercomToken()
|
||||
Intercom({
|
||||
app_id: 'ykeritl9',
|
||||
intercom_user_jwt: token,
|
||||
session_duration: 1000 * 60 * 60 * 24,
|
||||
alignment: 'right',
|
||||
horizontal_padding: sidebarVisible.value
|
||||
? INTERCOM_APP_SIDEBAR_WIDTH + INTERCOM_DEFAULT_PADDING
|
||||
: INTERCOM_DEFAULT_PADDING,
|
||||
vertical_padding: INTERCOM_DEFAULT_PADDING,
|
||||
})
|
||||
intercomBooted = true
|
||||
} catch (error) {
|
||||
console.warn('[APP][INTERCOM] failed to initialize secure support chat', error)
|
||||
} finally {
|
||||
intercomBooting = false
|
||||
}
|
||||
}
|
||||
|
||||
function shutdownHostingIntercom() {
|
||||
if (!intercomBooted && !intercomBooting) return
|
||||
shutdownIntercom()
|
||||
intercomBooting = false
|
||||
intercomBooted = false
|
||||
}
|
||||
|
||||
watch(
|
||||
sidebarVisible,
|
||||
(visible) => {
|
||||
if (intercomBooted) {
|
||||
window.Intercom?.('update', {
|
||||
horizontal_padding: visible
|
||||
? INTERCOM_APP_SIDEBAR_WIDTH + INTERCOM_DEFAULT_PADDING
|
||||
: INTERCOM_DEFAULT_PADDING,
|
||||
vertical_padding: INTERCOM_DEFAULT_PADDING,
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
[hostingRouteActive, credentials],
|
||||
([active]) => {
|
||||
if (active) {
|
||||
void bootIntercom()
|
||||
} else {
|
||||
shutdownHostingIntercom()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(showAd, () => {
|
||||
if (!showAd.value) {
|
||||
@@ -548,7 +786,9 @@ async function handleCommand(e) {
|
||||
if (e.event === 'RunMRPack') {
|
||||
// RunMRPack should directly install a local mrpack given a path
|
||||
if (e.path.endsWith('.mrpack')) {
|
||||
await create_profile_and_install_from_file(e.path).catch(handleError)
|
||||
await create_profile_and_install_from_file(e.path, (createProfile, fileName) =>
|
||||
unknownPackWarningModal.value?.show(createProfile, fileName),
|
||||
).catch(handleError)
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'CreationModalFileDrop',
|
||||
})
|
||||
@@ -970,9 +1210,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
:fetch-existing-instance-names="fetchExistingInstanceNames"
|
||||
:search-modpacks="searchModpacks"
|
||||
:get-project-versions="getProjectVersions"
|
||||
:get-loader-manifest="getLoaderManifest"
|
||||
@create="handleCreate"
|
||||
@browse-modpacks="handleBrowseModpacks"
|
||||
/>
|
||||
<UnknownPackWarningModal ref="unknownPackWarningModal" />
|
||||
<div
|
||||
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
|
||||
>
|
||||
@@ -982,13 +1224,6 @@ 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"
|
||||
@@ -1003,6 +1238,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<NavButton
|
||||
v-tooltip.right="'Library'"
|
||||
to="/library"
|
||||
:is-primary="(r) => r.path === '/library' || r.path === '/library'"
|
||||
:is-subpage="
|
||||
() =>
|
||||
route.path.startsWith('/instance') ||
|
||||
@@ -1012,6 +1248,14 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
>
|
||||
<LibraryIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Modrinth Hosting'"
|
||||
to="/hosting/manage"
|
||||
:is-primary="(r) => r.path === '/hosting/manage' || r.path === '/hosting/manage/'"
|
||||
:is-subpage="(r) => r.path.startsWith('/hosting/manage/') && r.path !== '/hosting/manage/'"
|
||||
>
|
||||
<ServerStackIcon />
|
||||
</NavButton>
|
||||
<div class="h-px w-6 mx-auto my-2 bg-surface-5"></div>
|
||||
<suspense>
|
||||
<QuickInstanceSwitcher />
|
||||
@@ -1127,25 +1371,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</ButtonStyled>
|
||||
<div class="flex mr-3">
|
||||
<Suspense>
|
||||
<RunningAppBar />
|
||||
<AppActionBar />
|
||||
</Suspense>
|
||||
</div>
|
||||
<section v-if="!nativeDecorations" class="window-controls" data-tauri-drag-region-exclude>
|
||||
<Button class="titlebar-button" icon-only @click="() => getCurrentWindow().minimize()">
|
||||
<MinimizeIcon />
|
||||
</Button>
|
||||
<Button
|
||||
class="titlebar-button"
|
||||
icon-only
|
||||
@click="() => getCurrentWindow().toggleMaximize()"
|
||||
>
|
||||
<RestoreIcon v-if="isMaximized" />
|
||||
<MaximizeIcon v-else />
|
||||
</Button>
|
||||
<Button class="titlebar-button close" icon-only @click="handleClose">
|
||||
<XIcon />
|
||||
</Button>
|
||||
</section>
|
||||
<WindowControls />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1181,14 +1410,14 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</div>
|
||||
</transition>
|
||||
<div
|
||||
class="loading-indicator-container h-8 fixed z-50"
|
||||
class="loading-indicator-container h-8 fixed z-50 pointer-events-none"
|
||||
:style="{
|
||||
top: 'calc(var(--top-bar-height))',
|
||||
left: 'calc(var(--left-bar-width))',
|
||||
width: 'calc(100% - var(--left-bar-width) - var(--right-bar-width))',
|
||||
}"
|
||||
>
|
||||
<ModrinthLoadingIndicator />
|
||||
<LoadingBar position="absolute" />
|
||||
</div>
|
||||
<div
|
||||
v-if="themeStore.featureFlags.page_path"
|
||||
@@ -1224,40 +1453,36 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</Admonition>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense @pending="loading.startLoading()" @resolve="loading.stopLoading()">
|
||||
<Suspense @pending="onSuspensePending" @resolve="onSuspenseResolve">
|
||||
<component :is="Component"></component>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</div>
|
||||
<div
|
||||
class="app-sidebar mt-px shrink-0 flex flex-col border-0 border-l-[1px] border-[--brand-gradient-border] border-solid overflow-auto"
|
||||
class="app-sidebar mt-px shrink-0 flex flex-col border-0 border-l-[1px] border-[--brand-gradient-border] border-solid"
|
||||
:class="{ 'has-plus': hasPlus }"
|
||||
>
|
||||
<div
|
||||
class="app-sidebar-scrollable flex-grow shrink overflow-y-auto relative"
|
||||
v-overlay-scrollbars="sidebarOverlayScrollbarsOptions"
|
||||
class="app-sidebar-scrollable flex-grow shrink relative"
|
||||
:class="{ 'pb-12': !hasPlus }"
|
||||
data-overlayscrollbars-initialize
|
||||
>
|
||||
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
|
||||
<div class="sidebar-default-content" :class="{ 'sidebar-enabled': sidebarVisible }">
|
||||
<div
|
||||
class="p-4 pr-1 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
|
||||
>
|
||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<h3 class="text-base text-primary font-medium m-0">Playing as</h3>
|
||||
<suspense>
|
||||
<AccountsCard ref="accounts" mode="small" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div class="py-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
<suspense>
|
||||
<FriendsList
|
||||
:credentials="credentials"
|
||||
:sign-in="() => signIn()"
|
||||
:refresh-credentials="fetchCredentials"
|
||||
/>
|
||||
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div v-if="news && news.length > 0" class="p-4 pr-1 flex flex-col items-center">
|
||||
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
|
||||
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
|
||||
<div class="space-y-4 flex flex-col items-center w-full">
|
||||
<NewsArticleCard
|
||||
@@ -1287,8 +1512,8 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</div>
|
||||
</div>
|
||||
<I18nDebugPanel />
|
||||
<NotificationPanel has-sidebar />
|
||||
<PopupNotificationPanel has-sidebar />
|
||||
<NotificationPanel :has-sidebar="sidebarVisible" />
|
||||
<PopupNotificationPanel :has-sidebar="sidebarVisible" />
|
||||
<ErrorModal ref="errorModal" />
|
||||
<MinecraftAuthErrorModal ref="minecraftAuthErrorModal" />
|
||||
<ContentInstallModal
|
||||
@@ -1324,72 +1549,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.window-controls {
|
||||
z-index: 20;
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.titlebar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all ease-in-out 0.1s;
|
||||
background-color: transparent;
|
||||
color: var(--color-base);
|
||||
height: 100%;
|
||||
width: 3rem;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0.75rem;
|
||||
width: 3.75rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
border-radius: 999999px;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
aspect-ratio: 1 / 1;
|
||||
margin-block: auto;
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
scale: 0.9;
|
||||
transition: all ease-in-out 0.2s;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&.close {
|
||||
&:hover,
|
||||
&:active {
|
||||
color: var(--color-accent-contrast);
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: var(--color-contrast);
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-button-bg);
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-grid-layout,
|
||||
.app-contents {
|
||||
--top-bar-height: 3rem;
|
||||
@@ -1414,6 +1573,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
|
||||
.app-grid-statusbar {
|
||||
grid-area: status;
|
||||
padding-right: var(--window-controls-width, 0px);
|
||||
}
|
||||
|
||||
[data-tauri-drag-region-exclude] {
|
||||
@@ -1605,6 +1765,13 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.os-theme-dark,
|
||||
.os-theme-light {
|
||||
--os-handle-bg: var(--color-scrollbar) !important;
|
||||
--os-handle-bg-hover: var(--color-scrollbar) !important;
|
||||
--os-handle-bg-active: var(--color-scrollbar) !important;
|
||||
}
|
||||
|
||||
.mac {
|
||||
.app-grid-statusbar {
|
||||
padding-left: 5rem;
|
||||
@@ -1616,10 +1783,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
right: 22rem;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@
|
||||
color-scheme: dark;
|
||||
--view-width: calc(100% - 5rem);
|
||||
--expanded-view-width: calc(100% - 13rem);
|
||||
--medal-promotion-bg: #000;
|
||||
--medal-promotion-bg-orange: rgba(208, 246, 255, 0.25);
|
||||
--medal-promotion-text-orange: #42abff;
|
||||
--medal-promotion-bg-gradient: linear-gradient(
|
||||
90deg,
|
||||
rgba(66, 170, 255, 0.15),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -77,12 +85,10 @@ body {
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-link);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
-webkit-font-smoothing: antialiased;
|
||||
will-change: filter;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -175,3 +181,55 @@ img {
|
||||
}
|
||||
|
||||
@import '@modrinth/assets/omorphia.scss';
|
||||
|
||||
input {
|
||||
border-radius: var(--size-rounded-sm);
|
||||
box-sizing: border-box;
|
||||
border: 2px solid transparent;
|
||||
// safari iOS rounds inputs by default
|
||||
// set the appearance to none to prevent this
|
||||
appearance: none !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-text);
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: none;
|
||||
outline: 2px solid transparent;
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
min-height: 36px;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 0.25rem var(--color-brand-shadow);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled='true'] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus::placeholder {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-button-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TrashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
DropdownSelect,
|
||||
formatLoader,
|
||||
injectNotificationManager,
|
||||
@@ -133,12 +134,33 @@ const state = useStorage(
|
||||
{
|
||||
group: 'Group',
|
||||
sortBy: 'Name',
|
||||
collapsedGroups: [],
|
||||
},
|
||||
localStorage,
|
||||
{ mergeDefaults: true },
|
||||
)
|
||||
|
||||
const search = ref('')
|
||||
const collapsedSectionKeys = computed(() => new Set(state.value.collapsedGroups ?? []))
|
||||
|
||||
const getSectionKey = (sectionName) => `${state.value.group}:${sectionName}`
|
||||
|
||||
const isSectionCollapsed = (sectionName) => {
|
||||
return collapsedSectionKeys.value.has(getSectionKey(sectionName))
|
||||
}
|
||||
|
||||
const setSectionCollapsed = (sectionName, collapsed) => {
|
||||
const sectionKey = getSectionKey(sectionName)
|
||||
const collapsedSections = new Set(state.value.collapsedGroups ?? [])
|
||||
|
||||
if (collapsed) {
|
||||
collapsedSections.add(sectionKey)
|
||||
} else {
|
||||
collapsedSections.delete(sectionKey)
|
||||
}
|
||||
|
||||
state.value.collapsedGroups = [...collapsedSections]
|
||||
}
|
||||
|
||||
const filteredResults = computed(() => {
|
||||
const { group = 'Group', sortBy = 'Name' } = state.value
|
||||
@@ -280,18 +302,21 @@ const filteredResults = computed(() => {
|
||||
<span class="font-semibold text-secondary">{{ selected }}</span>
|
||||
</DropdownSelect>
|
||||
</div>
|
||||
<div
|
||||
<Accordion
|
||||
v-for="instanceSection in Array.from(filteredResults, ([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
}))"
|
||||
:key="instanceSection.key"
|
||||
:divider="instanceSection.key !== 'None'"
|
||||
:open-by-default="!isSectionCollapsed(instanceSection.key)"
|
||||
class="row"
|
||||
@on-open="setSectionCollapsed(instanceSection.key, false)"
|
||||
@on-close="setSectionCollapsed(instanceSection.key, true)"
|
||||
>
|
||||
<div v-if="instanceSection.key !== 'None'" class="divider">
|
||||
<p>{{ instanceSection.key }}</p>
|
||||
<hr aria-hidden="true" />
|
||||
</div>
|
||||
<template v-if="instanceSection.key !== 'None'" #title>
|
||||
<span class="text-base">{{ instanceSection.key }}</span>
|
||||
</template>
|
||||
<section class="instances">
|
||||
<Instance
|
||||
v-for="instance in instanceSection.value"
|
||||
@@ -301,7 +326,7 @@ const filteredResults = computed(() => {
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event, instance.path)"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</Accordion>
|
||||
<ConfirmDeleteInstanceModal ref="confirmModal" @delete="deleteProfile" />
|
||||
<ContextMenu ref="instanceOptions" @option-clicked="handleOptionsClick">
|
||||
<template #play> <PlayIcon /> Play </template>
|
||||
@@ -316,73 +341,7 @@ const filteredResults = computed(() => {
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: var(--color-gray);
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: inherit;
|
||||
margin: 1rem 1rem 0 !important;
|
||||
padding: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
|
||||
.iconified-input {
|
||||
flex-grow: 1;
|
||||
|
||||
input {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-dropdown {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
.group-dropdown {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
.labeled_button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.instances {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import { useLoading } from '@/store/state.js'
|
||||
|
||||
const props = defineProps({
|
||||
throttle: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 2,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'var(--loading-bar-gradient)',
|
||||
},
|
||||
})
|
||||
|
||||
const indicator = useLoadingIndicator({
|
||||
duration: props.duration,
|
||||
throttle: props.throttle,
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => indicator.clear)
|
||||
|
||||
const loading = useLoading()
|
||||
|
||||
watch(loading, (newValue) => {
|
||||
if (newValue.barEnabled) {
|
||||
if (newValue.loading) {
|
||||
indicator.start()
|
||||
} else {
|
||||
indicator.finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function useLoadingIndicator(opts) {
|
||||
const progress = ref(0)
|
||||
const isLoading = ref(false)
|
||||
const step = computed(() => 10000 / opts.duration)
|
||||
|
||||
let _timer = null
|
||||
let _throttle = null
|
||||
|
||||
function start() {
|
||||
clear()
|
||||
progress.value = 0
|
||||
if (opts.throttle) {
|
||||
_throttle = setTimeout(() => {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}, opts.throttle)
|
||||
} else {
|
||||
isLoading.value = true
|
||||
_startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
function finish() {
|
||||
progress.value = 100
|
||||
_hide()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
clearInterval(_timer)
|
||||
clearTimeout(_throttle)
|
||||
_timer = null
|
||||
_throttle = null
|
||||
}
|
||||
|
||||
function _increase(num) {
|
||||
progress.value = Math.min(100, progress.value + num)
|
||||
}
|
||||
|
||||
function _hide() {
|
||||
clear()
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
setTimeout(() => {
|
||||
progress.value = 0
|
||||
}, 400)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function _startTimer() {
|
||||
_timer = setInterval(() => {
|
||||
_increase(step.value)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return { progress, isLoading, start, finish, clear }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="loading-indicator-bar"
|
||||
:style="{
|
||||
'--_width': `${indicator.progress.value}%`,
|
||||
'--_height': `${indicator.isLoading.value ? props.height : 0}px`,
|
||||
'--_opacity': `${indicator.isLoading.value ? 1 : 0}`,
|
||||
top: `0`,
|
||||
right: `0`,
|
||||
left: `${props.offsetWidth}`,
|
||||
pointerEvents: 'none',
|
||||
width: `var(--_width)`,
|
||||
height: `var(--_height)`,
|
||||
borderRadius: `var(--_height)`,
|
||||
// opacity: `var(--_opacity)`,
|
||||
background: `${props.color}`,
|
||||
backgroundSize: `${(100 / indicator.progress.value) * 100}% auto`,
|
||||
transition: 'width 0.1s ease-in-out, height 0.1s ease-out',
|
||||
zIndex: 6,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.loading-indicator-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: var(--_width);
|
||||
bottom: 0;
|
||||
background-image: radial-gradient(80% 100% at 20% 0%, var(--color-brand) 0%, transparent 80%);
|
||||
opacity: calc(var(--_opacity) * 0.1);
|
||||
z-index: 5;
|
||||
transition:
|
||||
width 0.1s ease-in-out,
|
||||
opacity 0.1s ease-out;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,434 @@
|
||||
<template>
|
||||
<div class="flex gap-4 items-center">
|
||||
<ButtonStyled
|
||||
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
|
||||
color="brand"
|
||||
type="transparent"
|
||||
circular
|
||||
>
|
||||
<button v-tooltip="formatMessage(messages.viewActiveDownloads)" @click="openDownloadToast()">
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-if="offline" class="flex items-center gap-1">
|
||||
<UnplugIcon class="text-secondary" />
|
||||
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
|
||||
</div>
|
||||
<div
|
||||
class="flex border-solid border-surface-5 text-sm items-center gap-2 py-1.5 px-3 rounded-xl border"
|
||||
>
|
||||
<template v-if="selectedProcess">
|
||||
<OnlineIndicatorIcon />
|
||||
<div class="text-contrast flex items-center gap-2">
|
||||
<router-link
|
||||
v-tooltip="formatMessage(messages.viewInstance)"
|
||||
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
||||
class="hover:underline"
|
||||
>
|
||||
{{ selectedProcess.profile.name }}
|
||||
</router-link>
|
||||
<Dropdown
|
||||
v-if="currentProcesses.length > 1"
|
||||
placement="bottom"
|
||||
:triggers="['click']"
|
||||
:hide-triggers="['click']"
|
||||
@show="showProfiles = true"
|
||||
@hide="showProfiles = false"
|
||||
>
|
||||
<ButtonStyled type="transparent" circular size="small">
|
||||
<button
|
||||
v-tooltip="
|
||||
showProfiles
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
>
|
||||
<DropdownIcon :class="{ 'rotate-180': !!showProfiles }" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template #popper>
|
||||
<div class="flex w-[20rem] max-h-[24rem] flex-col gap-2 overflow-auto">
|
||||
<div
|
||||
v-for="process in currentProcesses"
|
||||
:key="process.uuid"
|
||||
class="flex w-full items-center gap-2 rounded-xl bg-surface-4 p-2 text-sm"
|
||||
>
|
||||
<button
|
||||
v-tooltip.left="
|
||||
process.uuid === selectedProcess.uuid
|
||||
? formatMessage(messages.primaryInstance)
|
||||
: formatMessage(messages.makePrimaryInstance)
|
||||
"
|
||||
class="flex flex-grow items-center gap-2"
|
||||
:class="{
|
||||
'active:scale-95 transition-transform': process.uuid !== selectedProcess.uuid,
|
||||
}"
|
||||
:disabled="process.uuid === selectedProcess.uuid"
|
||||
@click="selectProcess(process)"
|
||||
>
|
||||
<OnlineIndicatorIcon />
|
||||
<span class="mr-auto text-contrast flex items-center gap-2">
|
||||
{{ process.profile.name }}
|
||||
<StarIcon v-if="process.uuid === selectedProcess.uuid" class="text-orange" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.stopInstance)"
|
||||
class="active:scale-95 flex"
|
||||
@click.stop="stop(process)"
|
||||
>
|
||||
<StopCircleIcon class="text-red size-5" />
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.viewLogs)"
|
||||
class="active:scale-95 flex"
|
||||
@click.stop="goToTerminal(process.profile.path)"
|
||||
>
|
||||
<TerminalSquareIcon class="text-secondary size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.stopInstance)"
|
||||
class="active:scale-95 flex"
|
||||
@click="stop(selectedProcess)"
|
||||
>
|
||||
<StopCircleIcon class="text-red size-5" />
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.viewLogs)"
|
||||
class="active:scale-95 flex"
|
||||
@click="goToTerminal()"
|
||||
>
|
||||
<TerminalSquareIcon class="text-secondary size-5" />
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="size-2 rounded-full bg-secondary" />
|
||||
<span class="text-secondary"> {{ formatMessage(messages.noInstancesRunning) }} </span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DownloadIcon,
|
||||
DropdownIcon,
|
||||
OnlineIndicatorIcon,
|
||||
StarIcon,
|
||||
StopCircleIcon,
|
||||
TerminalSquareIcon,
|
||||
UnplugIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
injectPopupNotificationManager,
|
||||
type PopupNotification,
|
||||
type PopupNotificationProgressItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { Dropdown } from 'floating-vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { loading_listener, process_listener } from '@/helpers/events'
|
||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||
import { get_many as getInstances } from '@/helpers/profile.js'
|
||||
import type { LoadingBar } from '@/helpers/state'
|
||||
import { progress_bars_list } from '@/helpers/state'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const showProfiles = ref(false)
|
||||
|
||||
interface RunningProcess {
|
||||
uuid: string
|
||||
profile_path: string
|
||||
profile: GameInstance
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
offline: {
|
||||
id: 'app.action-bar.offline',
|
||||
defaultMessage: 'Offline',
|
||||
},
|
||||
viewInstance: {
|
||||
id: 'app.action-bar.view-instance',
|
||||
defaultMessage: 'View instance',
|
||||
},
|
||||
showMoreRunningInstances: {
|
||||
id: 'app.action-bar.show-more-running-instances',
|
||||
defaultMessage: 'Show more running instances',
|
||||
},
|
||||
hideMoreRunningInstances: {
|
||||
id: 'app.action-bar.hide-more-running-instances',
|
||||
defaultMessage: 'Hide more running instances',
|
||||
},
|
||||
primaryInstance: {
|
||||
id: 'app.action-bar.primary-instance',
|
||||
defaultMessage: 'Primary instance',
|
||||
},
|
||||
makePrimaryInstance: {
|
||||
id: 'app.action-bar.make-primary-instance',
|
||||
defaultMessage: 'Make primary instance',
|
||||
},
|
||||
stopInstance: {
|
||||
id: 'app.action-bar.stop-instance',
|
||||
defaultMessage: 'Stop instance',
|
||||
},
|
||||
viewLogs: {
|
||||
id: 'app.action-bar.view-logs',
|
||||
defaultMessage: 'View logs',
|
||||
},
|
||||
noInstancesRunning: {
|
||||
id: 'app.action-bar.no-instances-running',
|
||||
defaultMessage: 'No instances running',
|
||||
},
|
||||
downloadingJava: {
|
||||
id: 'app.action-bar.downloading-java',
|
||||
defaultMessage: 'Downloading Java {version}',
|
||||
},
|
||||
downloads: {
|
||||
id: 'app.action-bar.downloads',
|
||||
defaultMessage: 'Downloads',
|
||||
},
|
||||
viewActiveDownloads: {
|
||||
id: 'app.action-bar.view-active-downloads',
|
||||
defaultMessage: 'View active downloads',
|
||||
},
|
||||
})
|
||||
|
||||
const currentProcesses = ref<RunningProcess[]>([])
|
||||
const selectedProcess = ref<RunningProcess | undefined>()
|
||||
|
||||
const refresh = async () => {
|
||||
const processes = ((await getRunningProcesses().catch((error) => {
|
||||
handleError(error)
|
||||
return []
|
||||
})) ?? []) as Array<{ uuid: string; profile_path: string }>
|
||||
const paths = processes.map((process) => process.profile_path)
|
||||
const profiles: GameInstance[] = await getInstances(paths).catch((error) => {
|
||||
handleError(error)
|
||||
return []
|
||||
})
|
||||
|
||||
currentProcesses.value = processes
|
||||
.map((process) => {
|
||||
const profile = profiles.find((item) => process.profile_path === item.path)
|
||||
if (!profile) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...process,
|
||||
profile,
|
||||
}
|
||||
})
|
||||
.filter((process): process is RunningProcess => process !== null)
|
||||
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
||||
selectedProcess.value = currentProcesses.value[0]
|
||||
}
|
||||
}
|
||||
|
||||
await refresh()
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
function handleOffline() {
|
||||
offline.value = true
|
||||
}
|
||||
function handleOnline() {
|
||||
offline.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('offline', handleOffline)
|
||||
window.addEventListener('online', handleOnline)
|
||||
})
|
||||
|
||||
const unlistenProcess = await process_listener(async () => {
|
||||
await refresh()
|
||||
})
|
||||
|
||||
const stop = async (process: RunningProcess) => {
|
||||
try {
|
||||
await killProcess(process.uuid).catch(handleError)
|
||||
|
||||
trackEvent('InstanceStop', {
|
||||
loader: process.profile.loader,
|
||||
game_version: process.profile.game_version,
|
||||
source: 'AppBar',
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
await refresh()
|
||||
}
|
||||
|
||||
function goToTerminal(path?: string) {
|
||||
const selectedPath = path ?? selectedProcess.value?.profile.path
|
||||
if (!selectedPath) {
|
||||
return
|
||||
}
|
||||
router.push(`/instance/${encodeURIComponent(selectedPath)}/logs`)
|
||||
}
|
||||
|
||||
const currentLoadingBars = ref<LoadingBar[]>([])
|
||||
const notificationId = ref<string | number | null>(null)
|
||||
const dismissed = ref(false)
|
||||
|
||||
function getLoadingBarKey(loadingBar: LoadingBar): string {
|
||||
return `${loadingBar.loading_bar_uuid ?? loadingBar.id}`
|
||||
}
|
||||
|
||||
function getLoadingProgress(loadingBar: LoadingBar): number {
|
||||
if (!loadingBar.total || loadingBar.total <= 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.max(0, Math.min(1, (loadingBar.current ?? 0) / (loadingBar.total ?? 0)))
|
||||
}
|
||||
|
||||
function getLoadingText(loadingBar: LoadingBar): string {
|
||||
const percent = Math.floor(getLoadingProgress(loadingBar) * 100)
|
||||
return loadingBar.message ? `${percent}% ${loadingBar.message}` : `${percent}%`
|
||||
}
|
||||
|
||||
function getNotification(): PopupNotification | null {
|
||||
if (!notificationId.value) {
|
||||
return null
|
||||
}
|
||||
const notification = popupNotificationManager
|
||||
.getNotifications()
|
||||
.find((notification) => notification.id === notificationId.value)
|
||||
return notification ?? null
|
||||
}
|
||||
|
||||
function removeNotification(): void {
|
||||
if (!notificationId.value) {
|
||||
return
|
||||
}
|
||||
popupNotificationManager.removeNotification(notificationId.value)
|
||||
notificationId.value = null
|
||||
}
|
||||
|
||||
function buildDownloadItems(): PopupNotificationProgressItem[] {
|
||||
return currentLoadingBars.value.map((bar) => ({
|
||||
id: getLoadingBarKey(bar),
|
||||
title: bar.title ?? '',
|
||||
text: getLoadingText(bar),
|
||||
progress: getLoadingProgress(bar),
|
||||
waiting: !bar.total || bar.total <= 0,
|
||||
}))
|
||||
}
|
||||
|
||||
const hasVisibleActiveDownloadToasts = computed(() => !!getNotification())
|
||||
const hasActiveLoadingBars = computed(() => currentLoadingBars.value.length > 0)
|
||||
|
||||
function updateNotification(resummon = false): void {
|
||||
if (resummon) {
|
||||
dismissed.value = false
|
||||
}
|
||||
|
||||
if (currentLoadingBars.value.length === 0) {
|
||||
removeNotification()
|
||||
dismissed.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (notificationId.value && !getNotification()) {
|
||||
notificationId.value = null
|
||||
dismissed.value = true
|
||||
}
|
||||
|
||||
if (dismissed.value && !resummon) {
|
||||
return
|
||||
}
|
||||
|
||||
let notif = getNotification()
|
||||
const progressItems = buildDownloadItems()
|
||||
|
||||
if (notif) {
|
||||
notif.title = formatMessage(messages.downloads)
|
||||
notif.text = undefined
|
||||
notif.progressItems = progressItems
|
||||
notif.progress = undefined
|
||||
notif.waiting = undefined
|
||||
} else {
|
||||
notif = popupNotificationManager.addPopupNotification({
|
||||
title: formatMessage(messages.downloads),
|
||||
type: 'download',
|
||||
autoCloseMs: null,
|
||||
progressItems,
|
||||
})
|
||||
notificationId.value = notif.id
|
||||
}
|
||||
}
|
||||
|
||||
function formatLoadingBars(loadingBar: LoadingBar): LoadingBar {
|
||||
const formatted = { ...loadingBar }
|
||||
if (formatted.bar_type?.type === 'java_download') {
|
||||
formatted.title = formatMessage(messages.downloadingJava, {
|
||||
version: formatted.bar_type.version,
|
||||
})
|
||||
}
|
||||
if (formatted.bar_type?.profile_path) {
|
||||
formatted.title = formatted.bar_type.profile_path
|
||||
}
|
||||
if (formatted.bar_type?.pack_name) {
|
||||
formatted.title = formatted.bar_type.pack_name
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
async function refreshLoadingBars() {
|
||||
const bars: Record<string, LoadingBar> = await progress_bars_list().catch((error) => {
|
||||
handleError(error)
|
||||
return {}
|
||||
})
|
||||
|
||||
currentLoadingBars.value = Object.values(bars)
|
||||
.map(formatLoadingBars)
|
||||
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
|
||||
|
||||
currentLoadingBars.value.sort((a, b) => {
|
||||
const aKey = `${a.loading_bar_uuid ?? a.id ?? ''}`
|
||||
const bKey = `${b.loading_bar_uuid ?? b.id ?? ''}`
|
||||
return aKey.localeCompare(bKey)
|
||||
})
|
||||
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
await refreshLoadingBars()
|
||||
|
||||
const unlistenLoading = await loading_listener(async () => {
|
||||
await refreshLoadingBars()
|
||||
})
|
||||
|
||||
function openDownloadToast() {
|
||||
updateNotification(true)
|
||||
}
|
||||
|
||||
function selectProcess(process: RunningProcess) {
|
||||
selectedProcess.value = process
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
removeNotification()
|
||||
dismissed.value = false
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
unlistenProcess()
|
||||
unlistenLoading()
|
||||
})
|
||||
</script>
|
||||
@@ -3,6 +3,7 @@
|
||||
v-if="typeof to === 'string'"
|
||||
:to="to"
|
||||
v-bind="$attrs"
|
||||
:active-class="isSubpage ? '' : undefined"
|
||||
:class="{
|
||||
'router-link-active': isPrimary && isPrimary(route),
|
||||
'subpage-active': isSubpage && isSubpage(route),
|
||||
|
||||
@@ -1,476 +0,0 @@
|
||||
<template>
|
||||
<div class="action-groups">
|
||||
<ButtonStyled v-if="currentLoadingBars.length > 0" color="brand" type="transparent" circular>
|
||||
<button ref="infoButton" @click="toggleCard()">
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div v-if="offline" class="status">
|
||||
<UnplugIcon />
|
||||
<div class="running-text">
|
||||
<span> Offline </span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedProcess" class="status">
|
||||
<span class="circle running" />
|
||||
<div ref="profileButton" class="running-text">
|
||||
<router-link
|
||||
class="text-primary"
|
||||
:to="`/instance/${encodeURIComponent(selectedProcess.profile.path)}`"
|
||||
>
|
||||
{{ selectedProcess.profile.name }}
|
||||
</router-link>
|
||||
<div
|
||||
v-if="currentProcesses.length > 1"
|
||||
class="arrow button-base"
|
||||
:class="{ rotate: showProfiles }"
|
||||
@click="toggleProfiles()"
|
||||
>
|
||||
<DropdownIcon />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="'Stop instance'"
|
||||
icon-only
|
||||
class="icon-button stop"
|
||||
@click="stop(selectedProcess)"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</Button>
|
||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
||||
<TerminalSquareIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="status">
|
||||
<span class="circle stopped" />
|
||||
<span class="running-text"> No instances running </span>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="download">
|
||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
||||
<div v-for="loadingBar in currentLoadingBars" :key="loadingBar.id" class="info-text">
|
||||
<h3 class="info-title">
|
||||
{{ loadingBar.title }}
|
||||
</h3>
|
||||
<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>
|
||||
</transition>
|
||||
<transition name="download">
|
||||
<Card
|
||||
v-if="showProfiles === true && currentProcesses.length > 0"
|
||||
ref="profiles"
|
||||
class="profile-card"
|
||||
>
|
||||
<Button
|
||||
v-for="process in currentProcesses"
|
||||
:key="process.uuid"
|
||||
class="profile-button"
|
||||
@click="selectProcess(process)"
|
||||
>
|
||||
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
||||
<Button
|
||||
v-tooltip="'Stop instance'"
|
||||
icon-only
|
||||
class="icon-button stop"
|
||||
@click.stop="stop(process)"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip="'View logs'"
|
||||
icon-only
|
||||
class="icon-button"
|
||||
@click.stop="goToTerminal(process.profile.path)"
|
||||
>
|
||||
<TerminalSquareIcon />
|
||||
</Button>
|
||||
</Button>
|
||||
</Card>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
DownloadIcon,
|
||||
DropdownIcon,
|
||||
StopCircleIcon,
|
||||
TerminalSquareIcon,
|
||||
UnplugIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { loading_listener, process_listener } from '@/helpers/events'
|
||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||
import { get_many } from '@/helpers/profile.js'
|
||||
import { progress_bars_list } from '@/helpers/state.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const router = useRouter()
|
||||
const card = ref(null)
|
||||
const profiles = ref(null)
|
||||
const infoButton = ref(null)
|
||||
const profileButton = ref(null)
|
||||
const showCard = ref(false)
|
||||
|
||||
const showProfiles = ref(false)
|
||||
|
||||
const currentProcesses = ref([])
|
||||
const selectedProcess = ref()
|
||||
|
||||
const refresh = async () => {
|
||||
const processes = await getRunningProcesses().catch(handleError)
|
||||
const profiles = await get_many(processes.map((x) => x.profile_path)).catch(handleError)
|
||||
|
||||
currentProcesses.value = processes.map((x) => ({
|
||||
profile: profiles.find((prof) => x.profile_path === prof.path),
|
||||
...x,
|
||||
}))
|
||||
if (!selectedProcess.value || !currentProcesses.value.includes(selectedProcess.value)) {
|
||||
selectedProcess.value = currentProcesses.value[0]
|
||||
}
|
||||
}
|
||||
|
||||
await refresh()
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
offline.value = true
|
||||
})
|
||||
window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const unlistenProcess = await process_listener(async () => {
|
||||
await refresh()
|
||||
})
|
||||
|
||||
const stop = async (process) => {
|
||||
try {
|
||||
await killProcess(process.uuid).catch(handleError)
|
||||
|
||||
trackEvent('InstanceStop', {
|
||||
loader: process.profile.loader,
|
||||
game_version: process.profile.game_version,
|
||||
source: 'AppBar',
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
await refresh()
|
||||
}
|
||||
|
||||
const goToTerminal = (path) => {
|
||||
router.push(`/instance/${encodeURIComponent(path ?? selectedProcess.value.profile.path)}/logs`)
|
||||
}
|
||||
|
||||
const currentLoadingBars = ref([])
|
||||
|
||||
const refreshInfo = async () => {
|
||||
const currentLoadingBarCount = currentLoadingBars.value.length
|
||||
currentLoadingBars.value = Object.values(await progress_bars_list().catch(handleError))
|
||||
.map((x) => {
|
||||
if (x.bar_type.type === 'java_download') {
|
||||
x.title = 'Downloading Java ' + x.bar_type.version
|
||||
}
|
||||
if (x.bar_type.profile_path) {
|
||||
x.title = x.bar_type.profile_path
|
||||
}
|
||||
if (x.bar_type.pack_name) {
|
||||
x.title = x.bar_type.pack_name
|
||||
}
|
||||
|
||||
return x
|
||||
})
|
||||
.filter((bar) => bar?.bar_type?.type !== 'launcher_update')
|
||||
|
||||
currentLoadingBars.value.sort((a, b) => {
|
||||
if (a.loading_bar_uuid < b.loading_bar_uuid) {
|
||||
return -1
|
||||
}
|
||||
if (a.loading_bar_uuid > b.loading_bar_uuid) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
if (currentLoadingBars.value.length === 0) {
|
||||
showCard.value = false
|
||||
} else if (currentLoadingBarCount < currentLoadingBars.value.length) {
|
||||
showCard.value = true
|
||||
}
|
||||
}
|
||||
|
||||
await refreshInfo()
|
||||
const unlistenLoading = await loading_listener(async () => {
|
||||
await refreshInfo()
|
||||
})
|
||||
|
||||
const selectProcess = (process) => {
|
||||
selectedProcess.value = process
|
||||
showProfiles.value = false
|
||||
}
|
||||
|
||||
const handleClickOutsideCard = (event) => {
|
||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||
if (
|
||||
card.value &&
|
||||
card.value.$el !== event.target &&
|
||||
!elements.includes(card.value.$el) &&
|
||||
infoButton.value &&
|
||||
!infoButton.value.contains(event.target)
|
||||
) {
|
||||
showCard.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutsideProfile = (event) => {
|
||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||
if (
|
||||
profiles.value &&
|
||||
profiles.value.$el !== event.target &&
|
||||
!elements.includes(profiles.value.$el) &&
|
||||
!profileButton.value.contains(event.target)
|
||||
) {
|
||||
showProfiles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCard = async () => {
|
||||
showCard.value = !showCard.value
|
||||
showProfiles.value = false
|
||||
await refreshInfo()
|
||||
}
|
||||
|
||||
const toggleProfiles = async () => {
|
||||
if (currentProcesses.value.length === 1) return
|
||||
showProfiles.value = !showProfiles.value
|
||||
showCard.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', handleClickOutsideCard)
|
||||
window.addEventListener('click', handleClickOutsideProfile)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', handleClickOutsideCard)
|
||||
window.removeEventListener('click', handleClickOutsideProfile)
|
||||
unlistenProcess()
|
||||
unlistenLoading()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.action-groups {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-divider);
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
}
|
||||
|
||||
.running-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-xs);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||
user-select: none;
|
||||
|
||||
&.clickable:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.25rem;
|
||||
|
||||
&.running {
|
||||
background-color: var(--color-brand);
|
||||
}
|
||||
|
||||
&.stopped {
|
||||
background-color: var(--color-base);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
box-shadow: none;
|
||||
width: 1.25rem !important;
|
||||
height: 1.25rem !important;
|
||||
|
||||
svg {
|
||||
min-width: 1.25rem;
|
||||
}
|
||||
|
||||
&.stop {
|
||||
color: var(--color-red);
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
right: 2rem;
|
||||
z-index: 9;
|
||||
width: 20rem;
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-raised);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: auto;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 1px solid var(--color-divider);
|
||||
|
||||
&.hidden {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-option {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
:hover {
|
||||
background-color: var(--color-raised-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
display: block;
|
||||
|
||||
:deep(svg) {
|
||||
left: 1rem;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.download-enter-active,
|
||||
.download-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.download-enter-from,
|
||||
.download-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
width: 100%;
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: none;
|
||||
|
||||
.text {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 9;
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-raised);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 1px solid var(--color-divider);
|
||||
padding: var(--gap-md);
|
||||
|
||||
&.hidden {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<ProjectCard
|
||||
:title="project.name"
|
||||
:link="
|
||||
() => {
|
||||
emit('open')
|
||||
$router.push({
|
||||
path: `/project/${project.project_id ?? project.id}`,
|
||||
query: { i: props.instance ? props.instance.path : undefined },
|
||||
})
|
||||
}
|
||||
"
|
||||
:author="{ name: project.author, link: `https://modrinth.com/user/${project.author}` }"
|
||||
:icon-url="project.icon_url"
|
||||
:summary="project.summary"
|
||||
: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="
|
||||
['mod', 'modpack'].includes(projectType)
|
||||
? {
|
||||
clientSide: project.client_side?.[0],
|
||||
serverSide: project.server_side?.[0],
|
||||
}
|
||||
: undefined
|
||||
"
|
||||
layout="list"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled color="brand" type="outlined">
|
||||
<button
|
||||
:disabled="installed || installing"
|
||||
class="shrink-0 no-wrap"
|
||||
@click.stop="install()"
|
||||
>
|
||||
<SpinnerIcon v-if="installing" class="animate-spin" />
|
||||
<template v-else-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>
|
||||
</template>
|
||||
</ProjectCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { CheckIcon, DownloadIcon, PlusIcon, SpinnerIcon } 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'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
|
||||
const { install: installVersion } = injectContentInstall()
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
instance: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
featured: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
projectType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
activeLoader: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
activeGameVersion: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['open', 'install'])
|
||||
|
||||
const installing = ref(false)
|
||||
|
||||
async function install() {
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
props.project.project_id ?? props.project.id,
|
||||
null,
|
||||
props.instance ? props.instance.path : null,
|
||||
'SearchCard',
|
||||
(versionId) => {
|
||||
installing.value = false
|
||||
if (versionId) {
|
||||
emit('install', props.project.project_id ?? props.project.id)
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
{
|
||||
preferredLoader: props.activeLoader ?? undefined,
|
||||
preferredGameVersion: props.activeGameVersion ?? undefined,
|
||||
},
|
||||
).catch(handleError)
|
||||
}
|
||||
|
||||
const modpack = computed(() => props.project.project_types?.includes('modpack'))
|
||||
</script>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
|
||||
<script setup>
|
||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||
import { Button, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
||||
import { get_project, get_version } from '@/helpers/cache.js'
|
||||
import { get_categories } from '@/helpers/tags.js'
|
||||
import { get_project_v3, get_version } from '@/helpers/cache.js'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
@@ -14,26 +12,22 @@ const { install: installVersion } = injectContentInstall()
|
||||
const confirmModal = ref(null)
|
||||
const project = ref(null)
|
||||
const version = ref(null)
|
||||
const categories = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
defineExpose({
|
||||
async show(event) {
|
||||
if (event.event === 'InstallVersion') {
|
||||
version.value = await get_version(event.id, 'must_revalidate').catch(handleError)
|
||||
project.value = await get_project(version.value.project_id, 'must_revalidate').catch(
|
||||
project.value = await get_project_v3(version.value.project_id, 'must_revalidate').catch(
|
||||
handleError,
|
||||
)
|
||||
} else {
|
||||
project.value = await get_project(event.id, 'must_revalidate').catch(handleError)
|
||||
project.value = await get_project_v3(event.id, 'must_revalidate').catch(handleError)
|
||||
version.value = await get_version(
|
||||
project.value.versions[project.value.versions.length - 1],
|
||||
'must_revalidate',
|
||||
).catch(handleError)
|
||||
}
|
||||
categories.value = (await get_categories().catch(handleError)).filter(
|
||||
(cat) => project.value.categories.includes(cat.name) && cat.project_type === 'mod',
|
||||
)
|
||||
confirmModal.value.show()
|
||||
},
|
||||
})
|
||||
@@ -52,13 +46,22 @@ async function install() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="confirmModal" :header="`Install ${project?.title}`">
|
||||
<ModalWrapper ref="confirmModal" :header="`Install ${project?.name}`">
|
||||
<div class="modal-body">
|
||||
<SearchCard
|
||||
:project="project"
|
||||
<ProjectCard
|
||||
:title="project.name"
|
||||
:link="() => confirmModal.hide()"
|
||||
:icon-url="project.icon_url"
|
||||
:summary="project.summary"
|
||||
: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"
|
||||
layout="list"
|
||||
class="project-card"
|
||||
:categories="categories"
|
||||
@open="confirmModal.hide()"
|
||||
/>
|
||||
<div class="button-row">
|
||||
<div class="markdown-body">
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<section
|
||||
v-if="showControls"
|
||||
class="flex items-center gap-2 mr-1.5"
|
||||
data-tauri-drag-region-exclude
|
||||
>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button class="relative expanded-button" @click="() => getCurrentWindow().minimize()">
|
||||
<MinimizeIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button class="relative expanded-button" @click="() => getCurrentWindow().toggleMaximize()">
|
||||
<RestoreIcon v-if="isMaximized" />
|
||||
<MaximizeIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
color="red"
|
||||
color-fill="none"
|
||||
hover-color-fill="background"
|
||||
circular
|
||||
>
|
||||
<button class="relative expanded-button" @click="handleClose">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MaximizeIcon, MinimizeIcon, RestoreIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import { getOS } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const nativeDecorations = ref(true)
|
||||
const isMaximized = ref(false)
|
||||
const os = ref('')
|
||||
|
||||
const alwaysShowAppControls = computed(() => themeStore.getFeatureFlag('always_show_app_controls'))
|
||||
|
||||
const showControls = computed(
|
||||
() =>
|
||||
alwaysShowAppControls.value ||
|
||||
(!nativeDecorations.value && (os.value === 'Windows' || os.value === 'Linux')),
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
os.value = await getOS()
|
||||
|
||||
const settings = await getSettings()
|
||||
nativeDecorations.value = settings.native_decorations
|
||||
|
||||
if (os.value !== 'MacOS') {
|
||||
await getCurrentWindow().setDecorations(nativeDecorations.value)
|
||||
}
|
||||
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
|
||||
const unlisten = await getCurrentWindow().onResized(async () => {
|
||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
})
|
||||
|
||||
const handleClose = async () => {
|
||||
await saveWindowState(StateFlags.ALL)
|
||||
await getCurrentWindow().close()
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.expanded-button::before {
|
||||
inset: -6px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -288,7 +288,7 @@ const messages = defineMessages({
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 ml-2 mr-1">
|
||||
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 -ml-1">
|
||||
<template v-if="sortedFriends.length > 0">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
@@ -309,7 +309,7 @@ const messages = defineMessages({
|
||||
@keyup.esc="search = ''"
|
||||
/>
|
||||
</template>
|
||||
<h3 v-else class="ml-2 w-full text-base text-primary font-medium m-0">
|
||||
<h3 v-else class="w-full text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.friends) }}
|
||||
</h3>
|
||||
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
|
||||
@@ -331,11 +331,11 @@ const messages = defineMessages({
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 v-if="loading" class="ml-4 mr-1 text-base text-primary font-medium m-0">
|
||||
<h3 v-if="loading" class="text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.friends) }}
|
||||
</h3>
|
||||
<template v-if="loading">
|
||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse ml-4 mr-1">
|
||||
<div v-for="n in 5" :key="n" class="flex gap-2 items-center animate-pulse">
|
||||
<div class="min-w-9 min-h-9 bg-button-bg rounded-full"></div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="h-3 bg-button-bg rounded-full w-1/2 mb-1"></div>
|
||||
@@ -344,7 +344,7 @@ const messages = defineMessages({
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="sortedFriends.length === 0">
|
||||
<div class="text-sm ml-4 mr-1">
|
||||
<div class="text-sm">
|
||||
<div v-if="!userCredentials">
|
||||
<IntlFormatted :message-id="messages.signInToAddFriends">
|
||||
<template #link="{ children }">
|
||||
|
||||
@@ -106,7 +106,7 @@ const messages = defineMessages({
|
||||
:open-by-default="openByDefault"
|
||||
:force-open="isSearching"
|
||||
:button-class="
|
||||
'pl-4 pr-3 flex w-full items-center bg-transparent border-0 p-0' +
|
||||
'flex w-full items-center bg-transparent border-0 p-0' +
|
||||
(isSearching
|
||||
? ''
|
||||
: ' cursor-pointer hover:brightness-[--hover-brightness] active:scale-[0.98] transition-all')
|
||||
@@ -122,7 +122,7 @@ const messages = defineMessages({
|
||||
<div
|
||||
v-for="friend in friends"
|
||||
:key="friend.username"
|
||||
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full ml-4 mr-1"
|
||||
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
|
||||
@contextmenu.prevent.stop="
|
||||
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
|
||||
"
|
||||
|
||||
@@ -107,7 +107,7 @@ defineExpose({
|
||||
|
||||
const install = async () => {
|
||||
installing.value = true
|
||||
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
||||
await installMod(instance.value.path, selectedVersion.value.id, 'standalone').catch(handleError)
|
||||
installing.value = false
|
||||
onInstall.value(selectedVersion.value.id)
|
||||
incompatibleModal.value.hide()
|
||||
|
||||
@@ -116,7 +116,7 @@ async function install(instance) {
|
||||
return
|
||||
}
|
||||
|
||||
await installMod(instance.path, version.id).catch(handleError)
|
||||
await installMod(instance.path, version.id, 'standalone').catch(handleError)
|
||||
await installVersionDependencies(instance, version).catch(handleError)
|
||||
|
||||
instance.installedMod = true
|
||||
@@ -188,7 +188,7 @@ const createInstance = async () => {
|
||||
|
||||
const id = await create(name.value, gameVersion, loader, 'latest', icon.value).catch(handleError)
|
||||
|
||||
await installMod(id, versions.value[0].id).catch(handleError)
|
||||
await installMod(id, versions.value[0].id, 'standalone').catch(handleError)
|
||||
|
||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<NewModal ref="modal" :header="formatMessage(messages.header)" :on-hide="reset">
|
||||
<div class="max-w-[31rem] flex flex-col gap-6">
|
||||
<Admonition
|
||||
type="warning"
|
||||
:header="formatMessage(messages.warningTitle)"
|
||||
:body="formatMessage(messages.warningBody)"
|
||||
/>
|
||||
<div v-if="fileName" class="overflow-x-auto whitespace-nowrap text-sm text-secondary">
|
||||
{{ fileName }}
|
||||
</div>
|
||||
<div>
|
||||
<p class="mt-0 leading-tight">
|
||||
{{ formatMessage(messages.body) }}
|
||||
</p>
|
||||
<p class="text-orange font-semibold mb-0 leading-tight">
|
||||
{{ formatMessage(messages.malwareStatement) }}
|
||||
</p>
|
||||
</div>
|
||||
<Checkbox v-model="dontShowAgain" :label="formatMessage(messages.dontShowAgain)" />
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="cancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button :disabled="isProceeding" @click="proceed">
|
||||
<SpinnerIcon v-if="isProceeding" class="animate-spin" />
|
||||
<CircleArrowRightIcon v-else />
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CircleArrowRightIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
|
||||
|
||||
const dontShowAgain = ref(false)
|
||||
const modal = useTemplateRef('modal')
|
||||
const onProceed = ref<() => Promise<void>>()
|
||||
const isProceeding = ref(false)
|
||||
const fileName = ref('')
|
||||
|
||||
const messages = defineMessages({
|
||||
header: {
|
||||
id: 'unknown-pack-warning-modal.header',
|
||||
defaultMessage: 'Confirm installation',
|
||||
},
|
||||
warningTitle: {
|
||||
id: 'unknown-pack-warning-modal.warning.title',
|
||||
defaultMessage: 'Unknown file warning',
|
||||
},
|
||||
warningBody: {
|
||||
id: 'unknown-pack-warning-modal.warning.body',
|
||||
defaultMessage: `We couldn't find this file on Modrinth. We strongly recommend only installing files from sources you trust.`,
|
||||
},
|
||||
body: {
|
||||
id: 'unknown-pack-warning-modal.body',
|
||||
defaultMessage: `A file is only reviewed if it’s uploaded to Modrinth, regardless of its file format (including .mrpack).`,
|
||||
},
|
||||
malwareStatement: {
|
||||
id: 'unknown-pack-warning-modal.malware-statement',
|
||||
defaultMessage: `Malware is often distributed through modpack files by sharing them on platforms like Discord.`,
|
||||
},
|
||||
dontShowAgain: {
|
||||
id: 'unknown-pack-warning-modal.dont-show-again',
|
||||
defaultMessage: `Don't show this warning again`,
|
||||
},
|
||||
installAnyway: {
|
||||
id: 'unknown-pack-warning-modal.install-anyway',
|
||||
defaultMessage: `Install anyway`,
|
||||
},
|
||||
})
|
||||
|
||||
function show(createInstance: () => Promise<void>, selectedFileName = '') {
|
||||
onProceed.value = createInstance
|
||||
fileName.value = selectedFileName
|
||||
dontShowAgain.value = false
|
||||
|
||||
if (themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)) {
|
||||
// noinspection ES6MissingAwait
|
||||
createInstance()
|
||||
return
|
||||
}
|
||||
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function reset() {
|
||||
onProceed.value = undefined
|
||||
fileName.value = ''
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
modal.value?.hide()
|
||||
}
|
||||
|
||||
async function proceed() {
|
||||
if (!onProceed.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (dontShowAgain.value) {
|
||||
themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = true
|
||||
const settings = await getSettings()
|
||||
settings.feature_flags[skipUnknownPackWarningFeatureFlag] = true
|
||||
await setSettings(settings)
|
||||
}
|
||||
|
||||
const createInstance = onProceed.value
|
||||
modal.value?.hide()
|
||||
// noinspection ES6MissingAwait
|
||||
createInstance()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
@@ -198,45 +198,48 @@ const messages = defineMessages({
|
||||
<template>
|
||||
<ConfirmDeleteInstanceModal ref="deleteConfirmModal" @delete="removeProfile" />
|
||||
<div class="block">
|
||||
<div class="float-end ml-4 relative group">
|
||||
<OverflowMenu
|
||||
v-tooltip="formatMessage(messages.editIcon)"
|
||||
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||
:options="[
|
||||
{
|
||||
id: 'select',
|
||||
action: () => setIcon(),
|
||||
},
|
||||
{
|
||||
id: 'remove',
|
||||
color: 'danger',
|
||||
action: () => resetIcon(),
|
||||
shown: !!icon,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<Avatar
|
||||
:src="icon ? convertFileSrc(icon) : icon"
|
||||
size="108px"
|
||||
class="!border-4 group-hover:brightness-75"
|
||||
:tint-by="instance.path"
|
||||
no-shadow
|
||||
/>
|
||||
<div class="absolute top-0 right-0 m-2">
|
||||
<div
|
||||
class="p-2 m-0 text-primary flex items-center justify-center aspect-square bg-button-bg rounded-full border-button-border border-solid border-[1px] hovering-icon-shadow"
|
||||
<div class="float-end ml-10 relative group w-fit">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">Icon</span>
|
||||
<div class="group relative w-fit">
|
||||
<OverflowMenu
|
||||
v-tooltip="formatMessage(messages.editIcon)"
|
||||
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||
:options="[
|
||||
{
|
||||
id: 'select',
|
||||
action: () => setIcon(),
|
||||
},
|
||||
{
|
||||
id: 'remove',
|
||||
color: 'danger',
|
||||
action: () => resetIcon(),
|
||||
shown: !!icon,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<EditIcon aria-hidden="true" class="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<Avatar
|
||||
:src="icon ? convertFileSrc(icon) : icon"
|
||||
size="108px"
|
||||
class="transition-[filter] group-hover:brightness-75"
|
||||
:tint-by="instance.path"
|
||||
no-shadow
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 h-full w-full flex items-center justify-center opacity-0 transition-all group-hover:opacity-100"
|
||||
>
|
||||
<EditIcon aria-hidden="true" class="h-10 w-10 text-primary" />
|
||||
</div>
|
||||
<template #select>
|
||||
<UploadIcon />
|
||||
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||
</template>
|
||||
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||
</OverflowMenu>
|
||||
</div>
|
||||
<template #select>
|
||||
<UploadIcon />
|
||||
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||
</template>
|
||||
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||
</OverflowMenu>
|
||||
</div>
|
||||
</div>
|
||||
<label for="instance-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||
<label for="instance-name" class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.name) }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
@@ -249,76 +252,82 @@ const messages = defineMessages({
|
||||
/>
|
||||
</div>
|
||||
<template v-if="instance.install_stage == 'installed'">
|
||||
<div>
|
||||
<h2
|
||||
id="duplicate-instance-label"
|
||||
class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block"
|
||||
>
|
||||
<div class="flex flex-col gap-2.5 mt-6">
|
||||
<h2 id="duplicate-instance-label" class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.duplicateInstance) }}
|
||||
</h2>
|
||||
<p class="m-0 mb-2">
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||
aria-labelledby="duplicate-instance-label"
|
||||
:disabled="installing"
|
||||
class="w-max !shadow-none"
|
||||
@click="duplicateProfile"
|
||||
>
|
||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2.5 mt-6">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.libraryGroups) }}
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<Checkbox
|
||||
v-for="group in availableGroups"
|
||||
:key="group"
|
||||
:model-value="groups.includes(group)"
|
||||
:label="group"
|
||||
@click="toggleGroup(group)"
|
||||
/>
|
||||
<div class="flex gap-2 items-center">
|
||||
<StyledInput
|
||||
v-model="newCategoryInput"
|
||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||
class="w-full max-w-[300px]"
|
||||
@submit="() => addCategory"
|
||||
/>
|
||||
<ButtonStyled>
|
||||
<button class="w-fit !shadow-none" @click="() => addCategory()">
|
||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.libraryGroupsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5 mt-6">
|
||||
<h2 id="delete-instance-label" class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.deleteInstance) }}
|
||||
</h2>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||
aria-labelledby="duplicate-instance-label"
|
||||
:disabled="installing"
|
||||
@click="duplicateProfile"
|
||||
aria-labelledby="delete-instance-label"
|
||||
:disabled="removing"
|
||||
class="w-fit !shadow-none"
|
||||
@click="deleteConfirmModal.show()"
|
||||
>
|
||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||
<TrashIcon v-else />
|
||||
{{
|
||||
removing
|
||||
? formatMessage(messages.deletingInstanceButton)
|
||||
: formatMessage(messages.deleteInstanceButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<h2 class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.libraryGroups) }}
|
||||
</h2>
|
||||
<p class="m-0 mb-2">
|
||||
{{ formatMessage(messages.libraryGroupsDescription) }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<Checkbox
|
||||
v-for="group in availableGroups"
|
||||
:key="group"
|
||||
:model-value="groups.includes(group)"
|
||||
:label="group"
|
||||
@click="toggleGroup(group)"
|
||||
/>
|
||||
<div class="flex gap-2 items-center">
|
||||
<StyledInput
|
||||
v-model="newCategoryInput"
|
||||
:placeholder="formatMessage(messages.libraryGroupsEnterName)"
|
||||
@submit="() => addCategory"
|
||||
/>
|
||||
<ButtonStyled>
|
||||
<button class="w-fit" @click="() => addCategory()">
|
||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<h2 id="delete-instance-label" class="m-0 mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.deleteInstance) }}
|
||||
</h2>
|
||||
<p class="m-0 mb-2">
|
||||
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||
</p>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
aria-labelledby="delete-instance-label"
|
||||
:disabled="removing"
|
||||
@click="deleteConfirmModal.show()"
|
||||
>
|
||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||
<TrashIcon v-else />
|
||||
{{
|
||||
removing
|
||||
? formatMessage(messages.deletingInstanceButton)
|
||||
: formatMessage(messages.deleteInstanceButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -101,57 +101,57 @@ const messages = defineMessages({
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
<h2 class="m-0 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.hooks) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="my-2.5" />
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.hooksDescription) }}
|
||||
</p>
|
||||
<Checkbox v-model="overrideHooks" :label="formatMessage(messages.customHooks)" class="mt-2" />
|
||||
|
||||
<h2 class="mt-2 mb-1 text-lg font-extrabold text-contrast">
|
||||
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.preLaunch) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.preLaunchDescription) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
id="pre-launch"
|
||||
v-model="hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
:placeholder="formatMessage(messages.preLaunchEnter)"
|
||||
wrapper-class="w-full mt-2"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.preLaunchDescription) }}
|
||||
</p>
|
||||
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.wrapper) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.wrapperDescription) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
id="wrapper"
|
||||
v-model="hooks.wrapper"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
:placeholder="formatMessage(messages.wrapperEnter)"
|
||||
wrapper-class="w-full mt-2"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.wrapperDescription) }}
|
||||
</p>
|
||||
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast">
|
||||
<h2 class="mt-6 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.postExit) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.postExitDescription) }}
|
||||
</p>
|
||||
<StyledInput
|
||||
id="post-exit"
|
||||
v-model="hooks.post_exit"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideHooks"
|
||||
:placeholder="formatMessage(messages.postExitEnter)"
|
||||
wrapper-class="w-full mt-2"
|
||||
wrapper-class="w-full my-2.5"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.postExitDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -111,10 +111,10 @@ const messages = defineMessages({
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 id="project-name" class="m-0 mb-1 text-lg font-extrabold text-contrast block">
|
||||
<h2 id="project-name" class="m-0 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.javaInstallation) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2" />
|
||||
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2.5" />
|
||||
<template v-if="!overrideJavaInstall">
|
||||
<div class="flex my-2 items-center gap-2 font-semibold">
|
||||
<template v-if="javaInstall">
|
||||
@@ -144,10 +144,10 @@ const messages = defineMessages({
|
||||
</div>
|
||||
</template>
|
||||
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.javaMemory) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2" />
|
||||
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2.5" />
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="memory.maximum"
|
||||
@@ -159,7 +159,7 @@ const messages = defineMessages({
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.javaArguments) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
||||
@@ -171,10 +171,10 @@ const messages = defineMessages({
|
||||
placeholder="Enter java arguments..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2" />
|
||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2.5" />
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="envVars"
|
||||
|
||||
@@ -94,14 +94,14 @@ const messages = defineMessages({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Checkbox
|
||||
v-model="overrideWindowSettings"
|
||||
:label="formatMessage(messages.customWindowSettings)"
|
||||
/>
|
||||
<div class="mt-2 flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.fullscreen) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
@@ -120,9 +120,9 @@ const messages = defineMessages({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.width) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
@@ -139,9 +139,9 @@ const messages = defineMessages({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-4 justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 mb-1 text-lg font-extrabold text-contrast">
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.height) }}
|
||||
</h2>
|
||||
<p class="m-0">
|
||||
|
||||
@@ -177,7 +177,7 @@ const messages = defineMessages({
|
||||
>
|
||||
<ModrinthIcon class="w-6 h-6" />
|
||||
</button>
|
||||
<div>
|
||||
<div class="max-w-[200px]">
|
||||
<p class="m-0">Modrinth App {{ version }}</p>
|
||||
<p class="m-0">
|
||||
<span v-if="osPlatform === 'macos'">macOS</span>
|
||||
|
||||
@@ -31,7 +31,7 @@ const props = defineProps({
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
defineExpose({
|
||||
show: (e: MouseEvent) => {
|
||||
show: (e?: MouseEvent) => {
|
||||
modal.value?.show(e)
|
||||
},
|
||||
hide: () => {
|
||||
|
||||
@@ -1,13 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { Combobox, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||
import { Combobox, defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { ColorTheme } from '@/store/theme.ts'
|
||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const worldsInHomeFeatureFlag = 'worlds_in_home' as FeatureFlag
|
||||
const skipUnknownPackWarningFeatureFlag = 'skip_unknown_pack_warning' as FeatureFlag
|
||||
|
||||
const messages = defineMessages({
|
||||
colorThemeTitle: {
|
||||
id: 'app.appearance-settings.color-theme.title',
|
||||
defaultMessage: 'Color theme',
|
||||
},
|
||||
colorThemeDescription: {
|
||||
id: 'app.appearance-settings.color-theme.description',
|
||||
defaultMessage: 'Select your preferred color theme for Modrinth App.',
|
||||
},
|
||||
advancedRenderingTitle: {
|
||||
id: 'app.appearance-settings.advanced-rendering.title',
|
||||
defaultMessage: 'Advanced rendering',
|
||||
},
|
||||
advancedRenderingDescription: {
|
||||
id: 'app.appearance-settings.advanced-rendering.description',
|
||||
defaultMessage:
|
||||
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
|
||||
},
|
||||
hideNametagTitle: {
|
||||
id: 'app.appearance-settings.hide-nametag.title',
|
||||
defaultMessage: 'Hide nametag',
|
||||
},
|
||||
hideNametagDescription: {
|
||||
id: 'app.appearance-settings.hide-nametag.description',
|
||||
defaultMessage: 'Disables the nametag above your player on the skins page.',
|
||||
},
|
||||
nativeDecorationsTitle: {
|
||||
id: 'app.appearance-settings.native-decorations.title',
|
||||
defaultMessage: 'Native decorations',
|
||||
},
|
||||
nativeDecorationsDescription: {
|
||||
id: 'app.appearance-settings.native-decorations.description',
|
||||
defaultMessage: 'Use system window frame (app restart required).',
|
||||
},
|
||||
minimizeLauncherTitle: {
|
||||
id: 'app.appearance-settings.minimize-launcher.title',
|
||||
defaultMessage: 'Minimize launcher',
|
||||
},
|
||||
minimizeLauncherDescription: {
|
||||
id: 'app.appearance-settings.minimize-launcher.description',
|
||||
defaultMessage: 'Minimize the launcher when a Minecraft process starts.',
|
||||
},
|
||||
defaultLandingPageTitle: {
|
||||
id: 'app.appearance-settings.default-landing-page.title',
|
||||
defaultMessage: 'Default landing page',
|
||||
},
|
||||
defaultLandingPageDescription: {
|
||||
id: 'app.appearance-settings.default-landing-page.description',
|
||||
defaultMessage: 'Change the page to which the launcher opens on.',
|
||||
},
|
||||
defaultLandingPageHome: {
|
||||
id: 'app.appearance-settings.default-landing-page.home',
|
||||
defaultMessage: 'Home',
|
||||
},
|
||||
defaultLandingPageLibrary: {
|
||||
id: 'app.appearance-settings.default-landing-page.library',
|
||||
defaultMessage: 'Library',
|
||||
},
|
||||
selectOption: {
|
||||
id: 'app.appearance-settings.select-option',
|
||||
defaultMessage: 'Select an option',
|
||||
},
|
||||
jumpBackIntoWorldsTitle: {
|
||||
id: 'app.appearance-settings.jump-back-into-worlds.title',
|
||||
defaultMessage: 'Jump back into worlds',
|
||||
},
|
||||
jumpBackIntoWorldsDescription: {
|
||||
id: 'app.appearance-settings.jump-back-into-worlds.description',
|
||||
defaultMessage: 'Includes recent worlds in the "Jump back in" section on the Home page.',
|
||||
},
|
||||
toggleSidebarTitle: {
|
||||
id: 'app.appearance-settings.toggle-sidebar.title',
|
||||
defaultMessage: 'Toggle sidebar',
|
||||
},
|
||||
toggleSidebarDescription: {
|
||||
id: 'app.appearance-settings.toggle-sidebar.description',
|
||||
defaultMessage: 'Enables the ability to toggle the sidebar.',
|
||||
},
|
||||
unknownPackWarningTitle: {
|
||||
id: 'app.appearance-settings.unknown-pack-warning.title',
|
||||
defaultMessage: 'Warn me before installing unknown modpacks',
|
||||
},
|
||||
unknownPackWarningDescription: {
|
||||
id: 'app.appearance-settings.unknown-pack-warning.description',
|
||||
defaultMessage:
|
||||
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
|
||||
},
|
||||
})
|
||||
|
||||
const os = ref(await getOS())
|
||||
const settings = ref(await get())
|
||||
@@ -21,8 +114,10 @@ watch(
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Color theme</h2>
|
||||
<p class="m-0 mt-1">Select your preferred color theme for Modrinth App.</p>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.colorThemeTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
@@ -36,12 +131,13 @@ watch(
|
||||
system-theme-color="system"
|
||||
/>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Advanced rendering</h2>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.advancedRenderingTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">
|
||||
Enables advanced rendering such as blur effects that may cause performance issues without
|
||||
hardware-accelerated rendering.
|
||||
{{ formatMessage(messages.advancedRenderingDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -57,66 +153,107 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.hideNametagTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
||||
</div>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
|
||||
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.nativeDecorationsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Minimize launcher</h2>
|
||||
<p class="m-0 mt-1">Minimize the launcher when a Minecraft process starts.</p>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.minimizeLauncherTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.minimizeLauncherDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.defaultLandingPageTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.defaultLandingPageDescription) }}</p>
|
||||
</div>
|
||||
<Combobox
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="w-40"
|
||||
:options="['Home', 'Library'].map((v) => ({ value: v, label: v }))"
|
||||
class="max-w-40"
|
||||
:options="[
|
||||
{
|
||||
value: 'Home',
|
||||
label: formatMessage(messages.defaultLandingPageHome),
|
||||
},
|
||||
{
|
||||
value: 'Library',
|
||||
label: formatMessage(messages.defaultLandingPageLibrary),
|
||||
},
|
||||
]"
|
||||
:display-value="settings.default_page ?? 'Select an option'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
||||
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
||||
:model-value="themeStore.getFeatureFlag(worldsInHomeFeatureFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
||||
themeStore.featureFlags['worlds_in_home'] = newValue
|
||||
settings.feature_flags['worlds_in_home'] = newValue
|
||||
const newValue = !themeStore.getFeatureFlag(worldsInHomeFeatureFlag)
|
||||
themeStore.featureFlags[worldsInHomeFeatureFlag] = newValue
|
||||
settings.feature_flags[worldsInHomeFeatureFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||
<p class="m-0 mt-1">Enables the ability to toggle the sidebar.</p>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.unknownPackWarningTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.unknownPackWarningDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFeatureFlag)"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
const warnBeforeUnknownPackInstall = !!e
|
||||
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
|
||||
themeStore.featureFlags[skipUnknownPackWarningFeatureFlag] = skipUnknownPackWarning
|
||||
settings.feature_flags[skipUnknownPackWarningFeatureFlag] = skipUnknownPackWarning
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.toggleSidebarTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="toggle-sidebar"
|
||||
|
||||
@@ -52,127 +52,135 @@ watch(
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Window size</h2>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Fullscreen</h3>
|
||||
<p class="m-0 leading-tight">
|
||||
Overwrites the options.txt file to start in full screen when launched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Fullscreen</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
Overwrites the options.txt file to start in full screen when launched.
|
||||
</p>
|
||||
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
||||
</div>
|
||||
|
||||
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Width</h3>
|
||||
<p class="m-0 leading-tight">The width of the game window when launched.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Width</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The width of the game window when launched.
|
||||
</p>
|
||||
<StyledInput
|
||||
id="width"
|
||||
v-model="settings.game_resolution[0]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter width..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StyledInput
|
||||
id="width"
|
||||
v-model="settings.game_resolution[0]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter width..."
|
||||
/>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Height</h3>
|
||||
<p class="m-0 leading-tight">The height of the game window when launched.</p>
|
||||
</div>
|
||||
|
||||
<StyledInput
|
||||
id="height"
|
||||
v-model="settings.game_resolution[1]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter height..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="mt-2 m-0 text-base font-extrabold text-primary">Height</h3>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The height of the game window when launched.
|
||||
</p>
|
||||
<hr class="my-6 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Memory allocated</h2>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||
</div>
|
||||
|
||||
<StyledInput
|
||||
id="height"
|
||||
v-model="settings.game_resolution[1]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter height..."
|
||||
/>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Java arguments</h2>
|
||||
<StyledInput
|
||||
id="java-args"
|
||||
v-model="settings.launchArgs"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter java arguments..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Environmental variables</h2>
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="settings.envVars"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter environmental variables..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||
<hr class="my-6 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Memory allocated</h2>
|
||||
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Pre launch hook</h3>
|
||||
<StyledInput
|
||||
id="pre-launch"
|
||||
v-model="settings.hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter pre-launch command..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">Ran before the instance is launched.</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Java arguments</h2>
|
||||
<StyledInput
|
||||
id="java-args"
|
||||
v-model="settings.launchArgs"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter java arguments..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Wrapper hook</h3>
|
||||
<StyledInput
|
||||
id="wrapper"
|
||||
v-model="settings.hooks.wrapper"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter wrapper command..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">Wrapper command for launching Minecraft.</p>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4 mb-2 text-lg font-extrabold text-contrast">Environmental variables</h2>
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="settings.envVars"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter environmental variables..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
|
||||
<hr class="mt-4 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Hooks</h2>
|
||||
|
||||
<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>
|
||||
<StyledInput
|
||||
id="pre-launch"
|
||||
v-model="settings.hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter pre-launch command..."
|
||||
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>
|
||||
<StyledInput
|
||||
id="wrapper"
|
||||
v-model="settings.hooks.wrapper"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter wrapper command..."
|
||||
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>
|
||||
<StyledInput
|
||||
id="post-exit"
|
||||
v-model="settings.hooks.post_exit"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter post-exit command..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Post exit hook</h3>
|
||||
<StyledInput
|
||||
id="post-exit"
|
||||
v-model="settings.hooks.post_exit"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter post-exit command..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">Ran after the game closes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -25,26 +25,28 @@ watch(
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div v-for="option in options" :key="option" class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast capitalize">
|
||||
{{ option.replaceAll('_', ' ') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
:disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
|
||||
@click="setFeatureFlag(option, DEFAULT_FEATURE_FLAGS[option])"
|
||||
>
|
||||
Reset to default
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.getFeatureFlag(option)"
|
||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||
/>
|
||||
<div class="flex flex-col gap-2.5 min-w-[600px]">
|
||||
<div v-for="option in options" :key="option" class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast capitalize">
|
||||
{{ option.replaceAll('_', ' ') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
:disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
|
||||
@click="setFeatureFlag(option, DEFAULT_FEATURE_FLAGS[option])"
|
||||
>
|
||||
Reset to default
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.getFeatureFlag(option)"
|
||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,15 +21,21 @@ async function updateJavaVersion(version) {
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<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>
|
||||
<JavaSelector
|
||||
:id="'java-selector-' + javaVersion"
|
||||
v-model="javaVersions[javaVersion]"
|
||||
:version="javaVersion"
|
||||
@update:model-value="updateJavaVersion"
|
||||
/>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div
|
||||
v-for="(javaVersion, index) in [25, 21, 17, 8]"
|
||||
:key="`java-${javaVersion}`"
|
||||
class="flex flex-col gap-2.5"
|
||||
>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
||||
Java {{ javaVersion }} location
|
||||
</h2>
|
||||
<JavaSelector
|
||||
:id="'java-selector-' + javaVersion"
|
||||
v-model="javaVersions[javaVersion]"
|
||||
:version="javaVersion"
|
||||
@update:model-value="updateJavaVersion"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -43,7 +43,7 @@ async function onLocaleChange(newLocale: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Language</h2>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Language</h2>
|
||||
|
||||
<Admonition type="warning" class="mt-2 mb-4">
|
||||
{{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
|
||||
|
||||
@@ -25,8 +25,8 @@ watch(
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
||||
<p class="m-0 text-sm">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Personalized ads</h2>
|
||||
<p class="m-0 mt-1 text-sm">
|
||||
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||
option, you opt out and ads will no longer be shown based on your interests.
|
||||
</p>
|
||||
@@ -36,8 +36,8 @@ watch(
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
||||
<p class="m-0 text-sm">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Telemetry</h2>
|
||||
<p class="m-0 mt-1 text-sm">
|
||||
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||
customize your experience. By disabling this option, you opt out and your data will no
|
||||
longer be collected.
|
||||
@@ -48,8 +48,8 @@ watch(
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Discord RPC</h2>
|
||||
<p class="m-0 text-sm">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Discord RPC</h2>
|
||||
<p class="m-0 mt-1 text-sm">
|
||||
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
||||
longer show up as a game or app you are using on your Discord profile.
|
||||
</p>
|
||||
|
||||
@@ -62,67 +62,77 @@ async function findLauncherDir() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App directory</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The directory where the launcher stores all of its files. Changes will be applied after
|
||||
restarting the launcher.
|
||||
</p>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">App directory</h2>
|
||||
<StyledInput
|
||||
id="appDir"
|
||||
v-model="settings.custom_dir"
|
||||
:icon="BoxIcon"
|
||||
type="text"
|
||||
wrapper-class="w-full"
|
||||
>
|
||||
<template #right>
|
||||
<Button class="ml-1.5" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
</template>
|
||||
</StyledInput>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The directory where the launcher stores all of its files. Changes will be applied after
|
||||
restarting the launcher.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="m-1 my-2">
|
||||
<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 class="flex flex-col gap-2.5">
|
||||
<ConfirmModalWrapper
|
||||
ref="purgeCacheConfirmModal"
|
||||
title="Are you sure you want to purge the cache?"
|
||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||
:has-to-type="false"
|
||||
proceed-label="Purge cache"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
|
||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
||||
app to reload data. This may slow down the app temporarily.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast mt-4">Maximum concurrent downloads</h2>
|
||||
<Slider
|
||||
id="max-downloads"
|
||||
v-model="settings.max_concurrent_downloads"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
/>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
||||
value if you have a poor internet connection. (app restart required to take effect)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">Maximum concurrent writes</h2>
|
||||
<Slider
|
||||
id="max-writes"
|
||||
v-model="settings.max_concurrent_writes"
|
||||
:min="1"
|
||||
:max="50"
|
||||
:step="1"
|
||||
/>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ConfirmModalWrapper
|
||||
ref="purgeCacheConfirmModal"
|
||||
title="Are you sure you want to purge the cache?"
|
||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||
:has-to-type="false"
|
||||
proceed-label="Purge cache"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">App cache</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
||||
app to reload data. This may slow down the app temporarily.
|
||||
</p>
|
||||
</div>
|
||||
<button id="purge-cache" class="btn min-w-max" @click="$refs.purgeCacheConfirmModal.show()">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast mt-4">Maximum concurrent downloads</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
||||
value if you have a poor internet connection. (app restart required to take effect)
|
||||
</p>
|
||||
<Slider
|
||||
id="max-downloads"
|
||||
v-model="settings.max_concurrent_downloads"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
/>
|
||||
|
||||
<h2 class="mt-4 m-0 text-lg font-extrabold text-contrast">Maximum concurrent writes</h2>
|
||||
<p class="m-0 mt-1 mb-2 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||
</p>
|
||||
<Slider id="max-writes" v-model="settings.max_concurrent_writes" :min="1" :max="50" :step="1" />
|
||||
</template>
|
||||
|
||||
@@ -79,7 +79,7 @@ watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||
})
|
||||
})
|
||||
|
||||
populateJumpBackIn()
|
||||
await populateJumpBackIn()
|
||||
.catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { createConsoleState } from '@modrinth/ui'
|
||||
|
||||
import { clear_log_buffer, get_live_log_buffer, get_logs } from '@/helpers/logs'
|
||||
|
||||
type ConsoleState = ReturnType<typeof createConsoleState>
|
||||
|
||||
interface LogEntry {
|
||||
filename: string
|
||||
name?: string
|
||||
log_type: string
|
||||
stdout?: string
|
||||
age?: number
|
||||
live?: boolean
|
||||
}
|
||||
|
||||
interface InstanceConsoleEntry {
|
||||
liveConsole: ConsoleState
|
||||
historicalConsole: ConsoleState
|
||||
historicalCache: Map<string, string>
|
||||
logList: LogEntry[] | null
|
||||
}
|
||||
|
||||
const instances = new Map<string, InstanceConsoleEntry>()
|
||||
|
||||
function getOrCreate(profilePathId: string): InstanceConsoleEntry {
|
||||
let entry = instances.get(profilePathId)
|
||||
if (entry) return entry
|
||||
|
||||
entry = {
|
||||
liveConsole: createConsoleState(),
|
||||
historicalConsole: createConsoleState(),
|
||||
historicalCache: new Map(),
|
||||
logList: null,
|
||||
}
|
||||
instances.set(profilePathId, entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
async function hydrate(profilePathId: string): Promise<void> {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
if (entry.liveConsole.output.value.length > 0) return
|
||||
|
||||
const buffer = await get_live_log_buffer(profilePathId)
|
||||
if (buffer) {
|
||||
entry.liveConsole.addLegacyLog(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
async function getHistoricalLogs(profilePathId: string, instancePath: string): Promise<LogEntry[]> {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
if (entry.logList) return entry.logList
|
||||
|
||||
const logs: LogEntry[] = await get_logs(instancePath, false)
|
||||
entry.logList = logs
|
||||
|
||||
for (const log of logs) {
|
||||
if (log.stdout && log.stdout !== '') {
|
||||
entry.historicalCache.set(log.filename, log.stdout)
|
||||
}
|
||||
}
|
||||
|
||||
return logs
|
||||
}
|
||||
|
||||
function getHistoricalContent(profilePathId: string, filename: string): string | undefined {
|
||||
return instances.get(profilePathId)?.historicalCache.get(filename)
|
||||
}
|
||||
|
||||
function invalidate(profilePathId: string): void {
|
||||
const entry = instances.get(profilePathId)
|
||||
if (!entry) return
|
||||
entry.historicalCache.clear()
|
||||
entry.logList = null
|
||||
}
|
||||
|
||||
async function clearLive(profilePathId: string): Promise<void> {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
entry.liveConsole.clear()
|
||||
await clear_log_buffer(profilePathId).catch(() => {})
|
||||
}
|
||||
|
||||
async function destroy(profilePathId: string): Promise<void> {
|
||||
instances.delete(profilePathId)
|
||||
await clear_log_buffer(profilePathId).catch(() => {})
|
||||
}
|
||||
|
||||
export function useInstanceConsole(profilePathId: string) {
|
||||
const entry = getOrCreate(profilePathId)
|
||||
return {
|
||||
liveConsole: entry.liveConsole,
|
||||
historicalConsole: entry.historicalConsole,
|
||||
hydrate: () => hydrate(profilePathId),
|
||||
getHistoricalLogs: (instancePath: string) => getHistoricalLogs(profilePathId, instancePath),
|
||||
getHistoricalContent: (filename: string) => getHistoricalContent(profilePathId, filename),
|
||||
invalidate: () => invalidate(profilePathId),
|
||||
clearLive: () => clearLive(profilePathId),
|
||||
destroy: () => destroy(profilePathId),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
const trimTrailingSlash = (url: string) => url.replace(/\/$/, '')
|
||||
|
||||
const siteUrl = trimTrailingSlash(import.meta.env.MODRINTH_URL || 'https://modrinth.com')
|
||||
const labrinthBaseUrl = trimTrailingSlash(
|
||||
import.meta.env.MODRINTH_API_BASE_URL || 'https://api.modrinth.com',
|
||||
)
|
||||
const archonBaseUrl = trimTrailingSlash(
|
||||
import.meta.env.MODRINTH_ARCHON_BASE_URL || 'https://archon.modrinth.com',
|
||||
)
|
||||
|
||||
export const config = {
|
||||
siteUrl,
|
||||
stripePublishableKey:
|
||||
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY ||
|
||||
'pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b',
|
||||
labrinthBaseUrl,
|
||||
archonBaseUrl,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { OverlayScrollbars, type PartialOptions } from 'overlayscrollbars'
|
||||
import type { ObjectDirective } from 'vue'
|
||||
|
||||
const defaultOverlayScrollbarsOptions = Object.freeze<PartialOptions>({
|
||||
scrollbars: {
|
||||
theme: 'os-theme-dark',
|
||||
autoHide: 'leave',
|
||||
autoHideSuspend: true,
|
||||
},
|
||||
})
|
||||
|
||||
const mergeOptions = (options: PartialOptions = {}): PartialOptions => ({
|
||||
...defaultOverlayScrollbarsOptions,
|
||||
...options,
|
||||
scrollbars: {
|
||||
...defaultOverlayScrollbarsOptions.scrollbars,
|
||||
...(options.scrollbars ?? {}),
|
||||
},
|
||||
})
|
||||
|
||||
export const overlayScrollbarsDirective: ObjectDirective<HTMLElement, PartialOptions | undefined> =
|
||||
{
|
||||
mounted(el, binding) {
|
||||
OverlayScrollbars(el, mergeOptions(binding.value))
|
||||
},
|
||||
updated(el, binding) {
|
||||
if (binding.value === binding.oldValue) return
|
||||
const instance = OverlayScrollbars(el)
|
||||
instance?.options(mergeOptions(binding.value))
|
||||
},
|
||||
unmounted(el) {
|
||||
const instance = OverlayScrollbars(el)
|
||||
instance?.destroy()
|
||||
},
|
||||
}
|
||||
@@ -97,3 +97,23 @@ export async function warning_listener(callback) {
|
||||
export async function friend_listener(callback) {
|
||||
return await listen('friend', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'log' event
|
||||
/*
|
||||
LogPayload {
|
||||
profile_path_id: string,
|
||||
type: "log4j" | "legacy",
|
||||
// log4j fields (when type === "log4j"):
|
||||
timestamp_millis?: number,
|
||||
logger_name?: string,
|
||||
level?: string,
|
||||
thread_name?: string,
|
||||
message?: string,
|
||||
throwable?: string,
|
||||
// legacy fields (when type === "legacy"):
|
||||
message?: string,
|
||||
}
|
||||
*/
|
||||
export async function log_listener(callback) {
|
||||
return await listen('log', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
@@ -63,3 +63,13 @@ export async function delete_logs(profilePath) {
|
||||
export async function get_latest_log_cursor(profilePath, cursor) {
|
||||
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })
|
||||
}
|
||||
|
||||
/// Get all buffered live log lines for a profile from the Rust ring buffer
|
||||
export async function get_live_log_buffer(profilePath) {
|
||||
return await invoke('plugin:logs|logs_get_live_log_buffer', { profilePath })
|
||||
}
|
||||
|
||||
/// Clear the live log buffer for a profile on the Rust side
|
||||
export async function clear_log_buffer(profilePath) {
|
||||
return await invoke('plugin:logs|logs_clear_live_log_buffer', { profilePath })
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ interface PackProfileCreator {
|
||||
gameVersion: string
|
||||
modloader: InstanceLoader
|
||||
loaderVersion: string | null
|
||||
unknownFile: boolean
|
||||
}
|
||||
|
||||
interface PackLocationVersionId {
|
||||
@@ -69,7 +70,10 @@ export async function install_to_existing_profile(
|
||||
return await invoke('plugin:pack|pack_install', { location, profile: profilePath })
|
||||
}
|
||||
|
||||
export async function create_profile_and_install_from_file(path: string): Promise<void> {
|
||||
export async function create_profile_and_install_from_file(
|
||||
path: string,
|
||||
showUnknownPackWarningModal?: (createProfile: () => Promise<void>, fileName: string) => void,
|
||||
): Promise<void> {
|
||||
const location: PackLocationFile = {
|
||||
type: 'fromFile',
|
||||
path,
|
||||
@@ -78,13 +82,24 @@ export async function create_profile_and_install_from_file(path: string): Promis
|
||||
'plugin:pack|pack_get_profile_from_pack',
|
||||
{ location },
|
||||
)
|
||||
const profile = await create(
|
||||
profile_creator.name,
|
||||
profile_creator.gameVersion,
|
||||
profile_creator.modloader,
|
||||
profile_creator.loaderVersion,
|
||||
null,
|
||||
true,
|
||||
)
|
||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||
|
||||
const createProfile = async () => {
|
||||
const profile = await create(
|
||||
profile_creator.name,
|
||||
profile_creator.gameVersion,
|
||||
profile_creator.modloader,
|
||||
profile_creator.loaderVersion,
|
||||
null,
|
||||
true,
|
||||
)
|
||||
await invoke('plugin:pack|pack_install', { location, profile })
|
||||
}
|
||||
|
||||
if (profile_creator.unknownFile && showUnknownPackWarningModal) {
|
||||
const splitPath = path.split(/[\\/]/)
|
||||
const fileName = splitPath ? splitPath[splitPath.length - 1] : path
|
||||
showUnknownPackWarningModal(createProfile, fileName)
|
||||
} else {
|
||||
await createProfile()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,8 +184,18 @@ export async function update_project(path: string, projectPath: string): Promise
|
||||
|
||||
// Add a project to a profile from a version
|
||||
// Returns a path to the new project file
|
||||
export async function add_project_from_version(path: string, versionId: string): Promise<string> {
|
||||
return await invoke('plugin:profile|profile_add_project_from_version', { path, versionId })
|
||||
export type DownloadReason = 'standalone' | 'dependency' | 'modpack'
|
||||
|
||||
export async function add_project_from_version(
|
||||
path: string,
|
||||
versionId: string,
|
||||
reason: DownloadReason,
|
||||
): Promise<string> {
|
||||
return await invoke('plugin:profile|profile_add_project_from_version', {
|
||||
path,
|
||||
versionId,
|
||||
reason,
|
||||
})
|
||||
}
|
||||
|
||||
// Add a project to a profile from a path + project_type
|
||||
|
||||
@@ -5,15 +5,46 @@
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export interface LoadingBarType {
|
||||
type?: string
|
||||
version?: string
|
||||
profile_path?: string
|
||||
pack_name?: string
|
||||
}
|
||||
|
||||
export interface LoadingBar {
|
||||
id?: string | number
|
||||
loading_bar_uuid?: string | number
|
||||
title?: string
|
||||
message?: string
|
||||
current?: number
|
||||
total?: number
|
||||
bar_type?: LoadingBarType
|
||||
}
|
||||
|
||||
export type OpeningCommandEvent =
|
||||
| 'RunMRPack'
|
||||
| 'InstallServer'
|
||||
| 'InstallVersion'
|
||||
| 'InstallMod'
|
||||
| 'InstallModpack'
|
||||
| string
|
||||
|
||||
export interface OpeningCommand {
|
||||
event: OpeningCommandEvent
|
||||
id?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
// Initialize the theseus API state
|
||||
// This should be called during the initializion/opening of the launcher
|
||||
export async function initialize_state() {
|
||||
return await invoke('initialize_state')
|
||||
return await invoke<void>('initialize_state')
|
||||
}
|
||||
|
||||
// Gets active progress bars
|
||||
export async function progress_bars_list() {
|
||||
return await invoke('plugin:utils|progress_bars_list')
|
||||
return await invoke<Record<string, LoadingBar>>('plugin:utils|progress_bars_list')
|
||||
}
|
||||
|
||||
// Get opening command
|
||||
@@ -21,5 +52,5 @@ export async function progress_bars_list() {
|
||||
// This should be called once and only when the app is done booting up and ready to receive a command
|
||||
// Returns a Command struct- see events.js
|
||||
export async function get_opening_command() {
|
||||
return await invoke('plugin:utils|get_opening_command')
|
||||
return await invoke<OpeningCommand | null>('plugin:utils|get_opening_command')
|
||||
}
|
||||
@@ -5,6 +5,39 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "تعذر الوصول إلى خوادم المصادقة"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "أضف الخادم للنموذج"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "أضف الخوادم لنموذجك"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "أضف للنموذج"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "أضف للنموذج {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "مضاف"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "مضاف فعلا"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "استكشف محتوى"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "استكشف خوادم"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "إخفاء الخوادم المضافة"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "إخفاء المحتوى المضاف"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "تثبيت محتوى لنموذجك"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "أدخل وصف التعديل..."
|
||||
},
|
||||
@@ -30,19 +63,28 @@
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "سيتم حذف جميع البيانات الخاصة بمثيلك نهائيًا، بما في ذلك عوالمك والتكوينات وكل المحتوى المثبت."
|
||||
"message": "سيتم حذف جميع البيانات الخاصة لنموذجك نهائيًا، بما في ذلك عوالمك والتكوينات وكل المحتوى المثبت."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "لا يمكن التراجع عن هذا الإجراء"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "حذف المثيل"
|
||||
"message": "حذف النموذج"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "حذف المثيل"
|
||||
"message": "حذف النموذج"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "حُزْمَة التعديل هذه مثبته فعلًا في نموذج <bold>{instanceName}</bold>. هل انت متأكد بإرادة نسخه؟"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "إنشاء"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "تم تثبيت حزمة التعديل بالفعل"
|
||||
"message": "حُزْمَة التعديل مثبتة بالفعل"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "النموذج"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "مشروع"
|
||||
@@ -51,7 +93,7 @@
|
||||
"message": "تمت إضافة \"{name}\""
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "تمت إضافة {count} من المشاريع"
|
||||
"message": "تمت إضافة {count} مشروع"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "تحقق من المشاريع التي أستخدمها في حزمة التعديل الخاص بي!"
|
||||
@@ -62,6 +104,51 @@
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "تم الرفع بنجاح"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "أضف خادم"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "تصفح الخوادم"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "سيتم حذف '{name}' **نهائيا**, و لن هناك أي طريقة لاسترداده."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "هل أنت متيقِّن من رغبتك بحذف هذا العالم نهائيا؟"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "معدّل"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "مفصول"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "يعمل"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "الأصلي"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "أضف خادما أو تصفح لكي تبدأ"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "لم تتم إضافة أي من الخوادم أو العوالم"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "ستتم إزالة '{name}' من قائمتك, إضافة لما داخل اللعبة, و لن يكون هناك أي طريقة لاسترداده."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "ستتم إزالة '{name}' ({address}) من قائمتك, إضافة لما داخل اللعبة, و لن يكون هناك أي طريقة لاسترداده."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "هل أنت متيقِّن من رغبتك في إزالة {name}؟"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "ابحث بين ال{count} عوالم..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "هذا الخادم"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "المحتوى مطلوب"
|
||||
},
|
||||
@@ -81,10 +168,10 @@
|
||||
"message": "يتطلب هذا الخادم تعديلات للعب. انقر فوق \"تثبيت\" لإعداد الملفات المطلوبة من Modrinth، ثم قم بتشغيله مباشرة إلى الخادم."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "حزمة مشتركة"
|
||||
"message": "النماذج المشتركة"
|
||||
},
|
||||
"app.modal.install-to-play.shared-server-instance": {
|
||||
"message": "حزمة خادم مشتركة"
|
||||
"message": "نماذج الخادم مشتركة"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "عرض المحتويات"
|
||||
@@ -96,7 +183,7 @@
|
||||
"message": "يلزم التحديث"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "هناك تحديث لازم للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
|
||||
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "تم تفعيل وضع المطوّر."
|
||||
@@ -164,6 +251,24 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "أعد التحميل لتثبيت التحديث"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "مثال.مودرنث.جج"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "حدد خيارا"
|
||||
},
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "إصدار غير مطابق ({version})"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "لم يتم اللعب به"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "مفصول"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} متصل حاليا"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "إضافة صديق"
|
||||
},
|
||||
@@ -263,6 +368,12 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "تعديل العالم"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "إضافة ملفات ({completed}\\{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "حفظ ك..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "العنوان"
|
||||
},
|
||||
@@ -300,7 +411,7 @@
|
||||
"message": "نسخة النسخة"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "إنشاء."
|
||||
"message": "ينشئ نسخة من هذا النموذج شاملا عوالمه, تعديلاتها النصيه, إلخ..."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "تعديل الأيقونة"
|
||||
@@ -480,7 +591,7 @@
|
||||
"message": "يقدمها الخادم"
|
||||
},
|
||||
"search.filter.locked.server-environment.title": {
|
||||
"message": "يمكن إضافة التعديلات من جانب العميل فقط إلى مثيل الخادم"
|
||||
"message": "يمكن إضافة التعديلات **المحليه** فقط إلى نموذج الخادم"
|
||||
},
|
||||
"search.filter.locked.server-game-version.title": {
|
||||
"message": "يتم توفير نسخة اللعبة من قبل الخادم"
|
||||
|
||||
@@ -5,12 +5,147 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Připojení k autorizačním serverům se nezdařilo"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Přidat server do instance"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Přidat servery do instance"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Přidat do instalace"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Přidat do {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Přidáno"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Už přidáno"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Prozkoumat obsah"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Prozkoumat servery"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Skrýt přidané servery"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Skrýt nainstalovaný obsah"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Nainstalovat obsah do instnce"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Přidej popis modpacku..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Exportovat"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportovat modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Jméno Modpacku"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Jméno modpacku"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Vybrat soubory a složky co zahrnout do modpacku"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Číslo verze"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Všechna data z instance budou navždy smazána, včetně světů, nastavení a všeho ostatního."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Tato akce nemůže být vrácena"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "Odstranit instanci"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Odstanit instanci"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Tento modpack je už nainstalovaný v instanci <bold>{instanceName}</bold>. Opravdu ho chceš duplikovat?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Vytvořit"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Modpack už je nainstalovaný"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instalace"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projekt"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "\"{name}\" bylo přidáno"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "{count} projektů bylo přidáno"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "Podívejte se na projekty co používám ve svém modpacku!"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "Sdílení obsahu modpacku"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Úspěšně nahráno"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Přidat server"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Procházet servery"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "\"{name}\" bude **navždy smazáno** a nebude způsob, jak to obnovit."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Opravdu si jsi jistý, že chceš navždy smazat tenhle svět?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Módováno"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "Online"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Vanilla"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "Přidej nebo procházej servery"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Žádné servery nebo světy nebyly přidány"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "„{name}“ bude odstraněn z tvého seznamu, včetně hry, a nebude žádný způsob, jak ho obnovit."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Opravdu chceš odstranit {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Hledat ve světech {count}..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "tento server"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Nainstaluj ke hraní"
|
||||
},
|
||||
@@ -81,7 +216,7 @@
|
||||
"message": "Přidat přítele"
|
||||
},
|
||||
"friends.action.view-friend-requests": {
|
||||
"message": "{count} {count, plural, one {žádost} few {žádosti} other {žádostí}} o přátelství"
|
||||
"message": "{count} přátelé {count, plural, one {request} other {requests}}"
|
||||
},
|
||||
"friends.add-friend.submit": {
|
||||
"message": "Poslat žádost o přátelství"
|
||||
@@ -363,7 +498,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š se rovnou připojit do světa jednoho hráče pouze v Minecraftu 1.20+"
|
||||
"message": "V Minecraftu 1.20+ se dá rovnou přeskočit pouze do singleplayerových světů"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "Hrát instanci"
|
||||
|
||||
@@ -5,6 +5,27 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Kan ikke nå autentificeringsservere"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Tilføjet til {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Tilføjet"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Allerede tilføjet"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Opdag indhold"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Opdag servere"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Gem tilføjet servere"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Gem installeret indhold"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Indtast modpack beskrivelse..."
|
||||
},
|
||||
@@ -41,6 +62,12 @@
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Slet instance"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Denne modpack er allerede installeret i <bold>{instanceName}</bold> instancen. Er du sikker på du vil duplikere den?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Opret"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projekt"
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"message": "Server zu deiner Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Zu Instanz hinzufügen"
|
||||
"message": "Zur Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Zu {instanceName} hinzufügen"
|
||||
@@ -63,7 +63,7 @@
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Alle Daten für deine Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
|
||||
"message": "Alle Daten deiner Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Diese Aktion kann nicht rückgängig gemacht werden"
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Downloading Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Hide more running instances"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Make primary instance"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "No instances running"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Primary instance"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Show more running instances"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Stop instance"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "View active downloads"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "View instance"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "View logs"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Advanced rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Select your preferred color theme for Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Color theme"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Change the page to which the launcher opens on."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Home"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Library"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Default landing page"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Disables the nametag above your player on the skins page."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Hide nametag"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Includes recent worlds in the \"Jump back in\" section on the Home page."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Jump back into worlds"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimize the launcher when a Minecraft process starts."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimize launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Use system window frame (app restart required)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Native decorations"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Select an option"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Enables the ability to toggle the sidebar."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Toggle sidebar"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Warn me before installing unknown modpacks"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft authentication servers may be down right now. Check your internet connection and try again later."
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Discover servers"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hide added servers"
|
||||
"message": "Hide already added servers"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Hide installed content"
|
||||
"message": "Hide already installed content"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Install"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Installed"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installing"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installing modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Enter modpack description..."
|
||||
},
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader is provided by the server"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "A file is only reviewed if it’s uploaded to Modrinth, regardless of its file format (including .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Don't show this warning again"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirm installation"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Install anyway"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Malware is often distributed through modpack files by sharing them on platforms like Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "We couldn't find this file on Modrinth. We strongly recommend only installing files from sources you trust."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Unknown file warning"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
"message": "No se puede acceder a los servidores de autenticación"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Añadir servidor a la instancia"
|
||||
"message": "Añadir servidor a instancia"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Añadir servidor a tu instancia"
|
||||
"message": "Añade servidores a tu instancia"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Añadir a la instancia"
|
||||
"message": "Añadir a instancia"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Añadir a {instanceName}"
|
||||
@@ -21,7 +21,7 @@
|
||||
"message": "Añadido"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Ya está añadido"
|
||||
"message": "Ya se ha añadido"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Descubrir contenido"
|
||||
@@ -36,10 +36,10 @@
|
||||
"message": "Ocultar contenido instalado"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instalar contenido a una instancia"
|
||||
"message": "Instalar contenido a la instancia"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Colocá la descripción del modpack..."
|
||||
"message": "Introduce la descripción del modpack..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Exportar"
|
||||
@@ -54,10 +54,10 @@
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Selelecciona archivos y carpetas para incluir en el pack"
|
||||
"message": "Selecciona archivos y carpetas para incluirlos en el pack"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Nombre de la versión"
|
||||
"message": "Número de la versión"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
@@ -75,13 +75,13 @@
|
||||
"message": "Eliminar instancia"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Este modpack ya está instalado en la instancia <bold>{instanceName}</bold>. ¿Estás seguro qué quieres duplicarlo?"
|
||||
"message": "El modpack ya está instalado en la instancia <bold>{instanceName}</bold>. ¿Estás seguro de querer duplicarlo?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Crear"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Este modpack ya esta instalado"
|
||||
"message": "Este modpack ya está instalado"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instancia"
|
||||
@@ -90,19 +90,19 @@
|
||||
"message": "proyecto"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "\"{name}\" fue añadido"
|
||||
"message": "Se añadió \"{name}\""
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "Se agregaron {count} proyectos"
|
||||
"message": "Se añadieron {count} proyectos"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "¡Echa un vistazo a los proyectos que estoy usando en mi paquete de mods!"
|
||||
"message": "¡Mira a los proyectos que estoy usando en mi modpack!"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "Compartiendo contenido del modpack"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Subido correctamente"
|
||||
"message": "Cargado correctamente"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Añadir servidor"
|
||||
@@ -111,13 +111,13 @@
|
||||
"message": "Explorar servidores"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' será **permanentemente eliminado**, y habrá forma de recuperarlo."
|
||||
"message": "\"{name}\" se **eliminará permanentemente** y no habrá forma de recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "¿Estás seguro de qué quieres eliminar permanentemente este mundo?"
|
||||
"message": "¿Estás seguro de que quieres eliminar este mundo de forma permanente?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modeado"
|
||||
"message": "Con mods"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Sin conexión"
|
||||
@@ -132,19 +132,19 @@
|
||||
"message": "Añadir un servidor o explora para comenzar"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "No hay servidores o mundos añadidos"
|
||||
"message": "No hay servidores ni mundos añadidos"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
|
||||
"message": "\"{name}\" se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
|
||||
"message": "\"{name}\" ({address}) se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "¿Estás seguro de qué quieres eliminar {name}?"
|
||||
"message": "¿Estás seguro de que quieres eliminar {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Buscar {count} mundos..."
|
||||
"message": "Buscar en {count} mundos..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "este servidor"
|
||||
@@ -165,7 +165,7 @@
|
||||
"message": "Modpack requerido"
|
||||
},
|
||||
"app.modal.install-to-play.server-requires-mods": {
|
||||
"message": "Este servidor requiere mods para poder jugar. Haz click en Instalar para configurar los archivos requeridos desde Modrinth, después se ejecutará para entrar directamente al servidor."
|
||||
"message": "Este servidor requiere mods para poder jugar. Haz clic en Instalar para configurar los archivos requeridos desde Modrinth, después se ejecutará para entrar directamente al servidor."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Instancia compartida"
|
||||
@@ -261,7 +261,7 @@
|
||||
"message": "Versión {version} incompatible"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "No se ha jugado aún"
|
||||
"message": "Aún no se ha jugado"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Desconectado"
|
||||
@@ -285,7 +285,7 @@
|
||||
"message": "¡Podría ser distinto a su nombre de usuario de Minecraft!"
|
||||
},
|
||||
"friends.add-friend.username.placeholder": {
|
||||
"message": "Ingresa tu nombre de usuario de Modrinth..."
|
||||
"message": "Introduce el nombre de usuario de Modrinth..."
|
||||
},
|
||||
"friends.add-friend.username.title": {
|
||||
"message": "¿Cuál es el nombre de usuario de Modrinth de tu amigo?"
|
||||
|
||||
@@ -5,8 +5,53 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Todennuspalvelimiin ei saada yhteyttä"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Lisää palvelin instanssiin"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Lisää palvelimia instanssiisi"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Lisää instanssiin"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Lisää instanssiin {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Lisätty"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "On jo asennettu"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Löydä sisältöä"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Löydä palvelimia"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Piilota lisätyt palvelimet"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Piilota asennettu sisältö"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Lataa sisältöä instanssiin"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Lisää modipaketin kuvaus..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Vie"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Vie modipaketti"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack nimi"
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Valitse tiedostot ja kansiot pakettiin"
|
||||
@@ -17,12 +62,90 @@
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Kaikki sinun instanssisi data poistetaan pysyvästi, sisältäen maailmasi, konfiguraatiosi ja asennetun sisällön."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Tätä toimintoa ei voi peruuttaa"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "Poista instannsi"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Poista instanssi"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Tämä modipaketti on jo asennettu instanssiin <bold>{instanceName}</bold>. Oletko varma että haluat kopioida sen?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Luo"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Modipaketti on jo asennettu"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instanssi"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projekti"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "\"{name}\" lisättiin"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "{count} projektia lisättiin"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "Tsekkaa projektit joita käytän minun modipaketissani!"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Onnistuneesti ladattu"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Lisää palvelin"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Selaa palvelimia"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' tullaan **poistamaan pysyvästi**, eikä tule olemaan mitään tapaa palauttaa sitä."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Oletko varma että haluat pysyvästi poistaa tämän maailman?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modattu"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Offline-tilassa"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "Online-tilassa"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Vanilla"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "Lisää palvelin tai selaa aloittaaksesi"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Ei palvelimia tai maailmoja lisättynä"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' tullaan poistamaan sinun listaltasi, mukaan lukien pelin sisällä, eikä tule olemaan mitään tapaa palauttaa sitä."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) tullaan poistamaan sinun listaltasi, mukaan lukien pelin sisällä, eikä tule olemaan mitään tapaa palauttaa sitä."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Oletko varma että haluat poistaa {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Hae {count} maailmasta..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "tämän palvelimen"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Sisältö vaaditaan"
|
||||
},
|
||||
@@ -33,7 +156,7 @@
|
||||
"message": "Asenna"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "one {{count, plural, one {# mod} other {# modia}}"
|
||||
"message": "{count, plural, one {# mod} other {# modia}}"
|
||||
},
|
||||
"app.modal.install-to-play.required-modpack": {
|
||||
"message": "Vaadittu modipaketti"
|
||||
@@ -125,6 +248,24 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Lataa uudelleen asentaaksesi päivityksen"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Valitse vaihtoehto"
|
||||
},
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "Yhteensopimaton versio {version}"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "Ei pelattu vielä"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Offline-tilassa"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} paikalla"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Lisää ystävä"
|
||||
},
|
||||
@@ -224,6 +365,12 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "Muokkaa maailmaa"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "Lisätään tiedostoja ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "Tallenna nimellä..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Osoite"
|
||||
},
|
||||
@@ -441,7 +588,7 @@
|
||||
"message": "Palvelimen tarjoama"
|
||||
},
|
||||
"search.filter.locked.server-environment.title": {
|
||||
"message": "Voit lisätä vain paikallisia modeja palvelinasennukseen"
|
||||
"message": "Voit lisätä vain paikallisia modeja palvelin instanssiin"
|
||||
},
|
||||
"search.filter.locked.server-game-version.title": {
|
||||
"message": "Peliversio on palvelimen tarjoama"
|
||||
|
||||
@@ -5,30 +5,129 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Hindi maabot ang mga authentication server"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Idagdag ang server sa instansiya"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Idagdag ang mga server sa iyong instansiya"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Idagdag sa instansiya"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Idagdag sa {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Dinagdag"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Nadagdag na"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Tumuklas ng kontento"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Tumuklas ng mga server"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Taguin ang mga nadagdag na server"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Taguin ang mga na-install na kontento"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "I-install ang kontento sa instansiya"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Ilagay ang paglalarawan ng modpack..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Iluwas"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Iluwas ang modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Pangalan ng Modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Pangalan ng modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Pumili ng mga talaksan at folder na isasali sa pack"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numero ng bersiyon"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Ang lahat ng data ng iyong instansiya ay tuluyang mawawala, kabilang na ang iyong mga mundo, kumpigurasyon, at lahat ng na-install na kontento."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Hindi mababawi ang aksiyong ito"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "Tanggalin ang instansiya"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Tanggalin ang instansiya"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Naka-install na ang modpack sa instansiyang <bold>{instanceName}</bold>. Sigurado ka ba sa pag-ulit nito?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Ilikha"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Naka-install na ang modpack"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instansiya"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "proyekto"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "Ang \"{name}\" ay nadagdag"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "{count} proyekto ang nadagdag"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "Tingnan ang mga proyektong ginagamit ko sa aking modpack!"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "Binabahagi ang kontento ng modpack"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Matagumpay na na-upload"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Magdagdag ng server"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modded"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "Online"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Vanilla"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Sigurado ka bang gusto mong tanggalin ang {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Hanapin sa {count} mundo..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "server na ito"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Nangangailangan ng kontento"
|
||||
},
|
||||
@@ -66,7 +165,7 @@
|
||||
"message": "Kailangang mag-update upang malaro ang {name}. Mangyaring mag-update sa pinakabagong bersiyon upang ma-launch ang laro."
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Nakabukas ang moda ng nagdidibelop."
|
||||
"message": "Binuksan ang moda ng nagdi-develop."
|
||||
},
|
||||
"app.settings.downloading": {
|
||||
"message": "Dina-download ang v{version}"
|
||||
@@ -131,6 +230,18 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Handang ma-install ang update"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Pumili ng opsiyon"
|
||||
},
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "Di-magkatugmang bersiyong {version}"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Magdagdag ng kaibigan"
|
||||
},
|
||||
@@ -201,10 +312,10 @@
|
||||
"message": "Idagdag ang server"
|
||||
},
|
||||
"instance.add-server.resource-pack.disabled": {
|
||||
"message": "Hindi pinahihintulotan"
|
||||
"message": "Tanggihan"
|
||||
},
|
||||
"instance.add-server.resource-pack.enabled": {
|
||||
"message": "Pinahihintulotan"
|
||||
"message": "Payagan"
|
||||
},
|
||||
"instance.add-server.resource-pack.prompt": {
|
||||
"message": "Magpahintulot"
|
||||
@@ -216,7 +327,7 @@
|
||||
"message": "Baguhin ang server"
|
||||
},
|
||||
"instance.edit-world.hide-from-home": {
|
||||
"message": "Huwag ipakita sa Home na pahina"
|
||||
"message": "Itago sa pahina ng Tahanan"
|
||||
},
|
||||
"instance.edit-world.name": {
|
||||
"message": "Pangalan"
|
||||
@@ -399,7 +510,7 @@
|
||||
"message": "Kopyahin ang adres"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Huwag ipakita sa Home"
|
||||
"message": "Huwag ipakita sa Tahanan"
|
||||
},
|
||||
"instance.worlds.game_already_open": {
|
||||
"message": "Bukas naman ang instansiya"
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"message": "Impossible de contacter les serveurs d'authentification"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Ajouter le serveur a l'instance"
|
||||
"message": "Ajouter le serveur à l'instance"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Ajouter des serveurs a votre instance"
|
||||
"message": "Ajouter des serveurs à votre instance"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Ajouter à l'instance"
|
||||
@@ -63,7 +63,7 @@
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Toutes les données pour votre instance seront supprimée à jamais, y comprit vos mondes, configurations, et contenu supprimé."
|
||||
"message": "Toutes les données pour votre instance seront supprimées à jamais, y compris vos mondes, vos configurations, et le contenu installé."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Cette action est irréversible"
|
||||
@@ -99,7 +99,7 @@
|
||||
"message": "Jette un coup d'œil aux projets que j'utilise dans mon modpack !"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "Partagez le contenu de votre modpack"
|
||||
"message": "Le contenu de mon modpack"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Mis en ligne avec succès"
|
||||
@@ -114,7 +114,7 @@
|
||||
"message": "« {name} » sera supprimé **pour toujours**, et il sera impossible de le récupérer."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Étes-vous sûr.e de vouloir supprimer ce monde pour toujours ?"
|
||||
"message": "Êtes-vous sûr.e de vouloir supprimer ce monde pour toujours ?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Moddé"
|
||||
@@ -210,7 +210,7 @@
|
||||
"message": "Gestion des ressources"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App v{version} est prête à être installée ! Rechargez pour mettre à jour maintenant, ou automatiquement quand vous fermez Modrinth App."
|
||||
"message": "Modrinth App v{version} est prête à être installée ! Relancez l'application pour faire la mise à jour maintenant, ou automatiquement à la fermeture de Modrinth App."
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} a finie d'être téléchargée. Rechargez pour mettre à jour maintenant, ou automatiquement quand vous fermez Modrinth App."
|
||||
|
||||
@@ -5,9 +5,39 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "לא ניתן לגשת לשרתי האימות"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "הוסף שרת להתקנה"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "הוסף שרתים להתקנה שלך"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "הוספה להתקנה"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "הוסף אל {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "נוסף"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "כבר נוסף"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "גלה תוכן"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "גלה שרתים"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "הסתר שרתים שנוספו"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "הסתר תוכן מותקן"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "הוספת התוכן להתקנה"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "הזן את תיאור חבילת המודים..."
|
||||
},
|
||||
@@ -44,9 +74,18 @@
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "מחיקת התקנה"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "חבילת המודים הזו כבר מותקנת בתוך ההתקנה <bold>{instanceName}</bold>. האם אתה בטוח שברצונך לשכפל אותה?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "צור"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "חבילת המודים הזאת כבר מותקנת"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "התקנה"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "פרויקט"
|
||||
},
|
||||
@@ -68,12 +107,48 @@
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "הוסף שרת"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "עיון בשרתים"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' יימחק לצמיתות, ולא תהיה דרך לשחזר אותו."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "האם אתה בטוח שברצונך למחוק לצמיתות את העולם הזה?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "עם מודים"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "לא מקוון"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "מחובר"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "ונילה"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "הוספת שרת או עיון כדי להתחיל"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "אין שרתים אן עולמות שנוספו"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' יוסר מהרשימה שלך, כולל מתוך המשחק, ולא תהיה דרך לשחזר אותו."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) יוסר מהרשימה שלך, כולל מתוך המשחק, ולא תהיה דרך לשחזר אותו."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "אתה בטוח שאתה רוצה להסיר {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "חיפוש ב-{count} עולמות..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "השרת הזה"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "תוכן נדרש"
|
||||
},
|
||||
@@ -176,6 +251,24 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "צריך לרענן כדי להתקין את העדכון"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "בחר אופציה"
|
||||
},
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "גרסה לא תואמת: {version}"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "עדיין לא שוחק"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "לא מקוון"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} מחוברים"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "הוספת חבר"
|
||||
},
|
||||
@@ -275,6 +368,9 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "ערוך עולם"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "הוספת קבצים ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "שמור כ..."
|
||||
},
|
||||
|
||||
@@ -134,6 +134,12 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Nincs szerver vagy világ"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' eltávolításra kerül a listádról, beleértve a játékon belülieket is. Ezt a lépést nem tudod visszavonni."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) eltávolításra kerül a listádról, beleértve a játékon belülieket is. Ezt a lépést nem tudod visszavonni."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Biztosan el akarod távolítani ezt: {name}?"
|
||||
},
|
||||
@@ -222,7 +228,7 @@
|
||||
"message": "Letöltés ({size})"
|
||||
},
|
||||
"app.update-popup.download-complete": {
|
||||
"message": "Letöltés sikeres"
|
||||
"message": "Sikeres letöltés"
|
||||
},
|
||||
"app.update-popup.reload": {
|
||||
"message": "Újratöltés"
|
||||
@@ -246,7 +252,7 @@
|
||||
"message": "A telepítéshez újraindítás szükséges"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
"message": "pelda.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Válassz egy lehetőséget"
|
||||
@@ -546,13 +552,13 @@
|
||||
"message": "Hardcore mód"
|
||||
},
|
||||
"instance.worlds.incompatible_server": {
|
||||
"message": "A Szerver nem kompatibilis"
|
||||
"message": "A szerver nem kompatibilis"
|
||||
},
|
||||
"instance.worlds.linked_server": {
|
||||
"message": "Szerverprojekt által kezelt"
|
||||
},
|
||||
"instance.worlds.no_contact": {
|
||||
"message": "Nem lehet kapcsolatot létesíteni a szerverrel"
|
||||
"message": "A szerverrel nem lehet kapcsolatot létesíteni"
|
||||
},
|
||||
"instance.worlds.no_server_quick_play": {
|
||||
"message": "Csak Minecraft Alpha 1.0.5+-tól tudsz egyből szerverhez csatlakozni"
|
||||
@@ -591,6 +597,6 @@
|
||||
"message": "A játékverziót a szerver biztosítja"
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "A betöltő a szerver által van megadva"
|
||||
"message": "A betöltőt a szerver biztosítja"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,39 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Tidak dapat terhubung ke server autentikasi"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Tambah server ke instans"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Tambah server ke instans Anda"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Tambah ke instans"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Tambah ke {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Ditambahkan"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Telah ditambahkan"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Temukan konten"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Temukan server"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Sembuyikan server tertambah"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Sembunyikan konten terpasang"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Pasang konten ke instans"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Masukkan deskripsi paket mod..."
|
||||
},
|
||||
@@ -41,9 +74,18 @@
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Hapus instans"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Paket mod ini telah terpasang di instans <bold>{instanceName}</bold>. Apakah Anda yakin ingin menggandakannya?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Buat"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Paket mod telah terpasang"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instans"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "proyek"
|
||||
},
|
||||
@@ -62,6 +104,51 @@
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Berhasil diunggah"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Tambah server"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Telusuri server"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' akan **dihapus secara permanen**, dan Anda tidak akan dapat memulihkannya."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Apakah Anda yakin ingin menghapus dunia ini secara permanen?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Dimodifikasi"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Luring"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "Daring"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Vanila"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "Tambah server atau telusuri untuk memulai"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Tidak ada server atau dunia yang ditambahkan"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' akan dihapus dari daftar Anda, termasuk di dalam permainan, dan Anda tidak akan dapat memulihkannya."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) akan dihapus dari daftar Anda, termasuk di dalam permainan, dan Anda tidak akan dapat memulihkannya."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Apakah Anda yakin ingin menghapus {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Cari {count} dunia..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "server ini"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Konten diperlukan"
|
||||
},
|
||||
@@ -164,6 +251,24 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Muat ulang untuk memasang pembaruan"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Pilih opsi"
|
||||
},
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "Versi {version} tidak cocok"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "Belum pernah dimainkan"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Luring"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} orang daring"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Tambah teman"
|
||||
},
|
||||
@@ -263,6 +368,12 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "Sunting dunia"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "Menambahkan berkas ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "Simpan sebagai..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Alamat"
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"message": "Aggiungi server all'istanza"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Aggiungi server alla tua istanza"
|
||||
"message": "Aggiungi i server alla tua istanza"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Aggiungi all'istanza"
|
||||
@@ -24,7 +24,7 @@
|
||||
"message": "Già aggiunto"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Esplora contenuti"
|
||||
"message": "Esplora i contenuti"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Esplora i server"
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Nascondi contenuti installati"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Installa nella istanza"
|
||||
"message": "Installa contenuti nell'istanza"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Inserisci descrizione del pacchetto..."
|
||||
@@ -111,10 +111,10 @@
|
||||
"message": "Esplora i server"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "\"{name}\" verrà eliminato permanentemente e non ci sarà modo di recuperarlo."
|
||||
"message": "\"{name}\" verrà **eliminato permanentemente** e non ci sarà modo di recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Vuoi davvero eliminare permanentemente questo mondo?"
|
||||
"message": "Vuoi davvero eliminare questo mondo per sempre?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Moddato"
|
||||
@@ -132,13 +132,13 @@
|
||||
"message": "Inizia esplorando o aggiungendo un server"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Nessun server o mondi aggiunti"
|
||||
"message": "Nessun server o mondo aggiunto"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "\"{name}\" sarà rimosso dalla tua lista, inclusa quella in-gioco, e non ci sarà modo di recuperarlo."
|
||||
"message": "\"{name}\" sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "\"{name}\" ({address}) sarà rimosso dalla tua lista, inclusa quella in-gioco, e non ci sarà modo di recuperarlo."
|
||||
"message": "\"{name}\" ({address}) sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Vuoi davvero rimuovere {name}?"
|
||||
@@ -246,7 +246,7 @@
|
||||
"message": "Scarica aggiornamento"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Scaricando aggiornamento ({percent}%)"
|
||||
"message": "Scaricando l'aggiornamento ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Ricarica per installare l'aggiornamento"
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"message": "オンライン"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "バニラ(Mod非導入)"
|
||||
"message": "バニラ"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "サーバーを追加、またはブラウズして始める"
|
||||
@@ -144,7 +144,7 @@
|
||||
"message": "本当に {name} を削除しますか?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "{count} 個のワールドを検索..."
|
||||
"message": "{count} 個のワールドを検索…"
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "このサーバー"
|
||||
@@ -372,7 +372,7 @@
|
||||
"message": "ファイルを追加中 ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "名前をつけて保存..."
|
||||
"message": "名前をつけて保存…"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "アドレス"
|
||||
|
||||
@@ -216,10 +216,10 @@
|
||||
"message": "Modrinth App v{version} 다운로드가 완료되었습니다. 새로고침하거나 Modrinth App을 종료하면 자동으로 업데이트됩니다."
|
||||
},
|
||||
"app.update-popup.body.linux": {
|
||||
"message": "Modrinth App v{version}을 사용할 수 있습니다. 최신 기능과 오류 수정을 적용하려면 패키지 관리자를 통해 업데이트하세요!"
|
||||
"message": "Modrinth 앱 v{version}을 사용할 수 있습니다. 최신 기능과 오류 수정을 적용하려면 패키지 관리자를 통해 업데이트하세요!"
|
||||
},
|
||||
"app.update-popup.body.metered": {
|
||||
"message": "Modrinth App v{version}을 지금 사용할 수 있습니다! 데이터 통신 연결을 사용 중이므로, 자동으로 다운로드하지 않았습니다."
|
||||
"message": "Modrinth 앱 v{version}을 지금 사용할 수 있습니다! 데이터 통신 연결을 사용 중이므로, 자동으로 다운로드하지 않았습니다."
|
||||
},
|
||||
"app.update-popup.changelog": {
|
||||
"message": "변경 내역"
|
||||
|
||||
@@ -5,6 +5,39 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Authenticatieservers kunnen niet worden bereikt"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Voeg server aan instantie toe"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Voeg servers aan instantie toe"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Voeg aan instantie toe"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Voeg aan {instanceName} toe"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Toegevoegd"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Al toegevoegd"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Ontdek content"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Ontdek servers"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Verstop toegevoegde servers"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Verberg Geïnstalleerde inhoud"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Installeer inhoud naar instantie"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Voeg modpack beschrijving in..."
|
||||
},
|
||||
@@ -41,9 +74,18 @@
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Verwijder instantie"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Deze modpakket is al in de <bold>{instanceName}<bold> instantie geïnstalleerd. Ben je zeker dat je het geen wil dupliceren?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Maak"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Modpack is al geïnstalleerd"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instantie"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "project"
|
||||
},
|
||||
@@ -51,7 +93,7 @@
|
||||
"message": "\"{name}\" was toegevoegd"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "{count} projecten waren toegevoegd"
|
||||
"message": "{count} Projecten waren toegevoegd"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "Bekijk de projecten die ik in mijn modpack gebruik!"
|
||||
@@ -62,6 +104,51 @@
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Succesvol geüpload"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Voeg server toe"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Zoek servers"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "{name} Zal **permanent verwijderd** worden en kan niet hersteld worden."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Weet je zeker dat je deze wereld definitief wilt verwijderen?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Gemod"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "Online"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Vanilla"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "Voeg of zoek een server om te beginnen"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Noch servers noch werelden toegevoegd"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' wordt uit je lijst verwijderd, ook in de game, en kan op geen enkele manier worden hersteld."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) wordt uit je lijst verwijderd, ook in de game, en kan op geen enkele manier worden hersteld."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Ben je zeker dat je {name} wil verwijderen?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Zoek werelden"
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "deze server"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Content vereist"
|
||||
},
|
||||
@@ -164,6 +251,24 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Herlaad om de update te installeren"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "voorbeeld.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Kies een keuze"
|
||||
},
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "Incompatibele versie{version}"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "Nog niet gespeeld"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} online"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Voeg een vriend toe"
|
||||
},
|
||||
@@ -263,6 +368,12 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "Wereld bewerken"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "Bestanden toevoegen ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "Opslaan als..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Adres"
|
||||
},
|
||||
@@ -345,7 +456,7 @@
|
||||
"message": "Uitgevoerd nadat het spel is afgesloten."
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.enter": {
|
||||
"message": "Voer na-afsluitcommando in..."
|
||||
"message": "Voer een opdracht na het afsluiten in..."
|
||||
},
|
||||
"instance.settings.tabs.hooks.pre-launch": {
|
||||
"message": "Voor het opstarten"
|
||||
@@ -411,7 +522,7 @@
|
||||
"message": "De hoogte van het spelvenster wanneer opgestart."
|
||||
},
|
||||
"instance.settings.tabs.window.height.enter": {
|
||||
"message": "Vul hoogte in..."
|
||||
"message": "Voer de lengte in..."
|
||||
},
|
||||
"instance.settings.tabs.window.width": {
|
||||
"message": "Breedte"
|
||||
@@ -453,7 +564,7 @@
|
||||
"message": "Je kan alleen direct de servers in springen in Minecraft Alpha 1.0.5+"
|
||||
},
|
||||
"instance.worlds.no_singleplayer_quick_play": {
|
||||
"message": "Je kan alleen direct je eigen werelden in springen in Minecraft 1.20+"
|
||||
"message": "Je kan alleen direct je eigen werelden in springen in Minecraft 1,20+"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "Speel instantie"
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"message": "Szukaj serwerów"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' zostanie **permanentnie usunięty**, i nie ma możliwości go odzyskać."
|
||||
"message": "'{name}' zostanie **permanentnie usunięty** i nie ma możliwości go odzyskać."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Czy na pewno chcesz trwale usunąć ten świat?"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Os servidores de autenticação do Minecraft podem estar indisponíveis no momento. Verifique sua conexão com a internet e tente novamente mais tarde."
|
||||
"message": "Os servidores de autenticação do Minecraft podem estar indisponíveis no momento. Verifique sua conexão com a rede e tente novamente mais tarde."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Não foi possível acessar os servidores de autenticação"
|
||||
@@ -39,7 +39,7 @@
|
||||
"message": "Instalar conteúdo na instância"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Insira a descrição do pacote de mods..."
|
||||
"message": "Insira uma descrição para o pacote de mods..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Exportar"
|
||||
@@ -102,7 +102,7 @@
|
||||
"message": "Compartilhando conteúdo do pacote de mods\n\n\n \t\t\t\t\t\t\n \t\t\t\t\t\t\t\n \t\t\t\t\t\t\n \t\t\t\t\t\t"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Enviado com sucesso"
|
||||
"message": "Adicionado com sucesso"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Adicionar servidor"
|
||||
|
||||
@@ -107,6 +107,9 @@
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Reîncarcă pentru a instala actualizarea"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Selectează o opțiune"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Adaugă un prieten"
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"message": "Скрывать установленные"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Установить контент в сборку"
|
||||
"message": "Установка контента в сборку"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Введите описание сборки..."
|
||||
@@ -45,7 +45,7 @@
|
||||
"message": "Экспортировать"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Экспорт сборки модов"
|
||||
"message": "Экспорт сборки"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Название сборки"
|
||||
@@ -54,7 +54,7 @@
|
||||
"message": "Название сборки"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Выберите файлы или папки для включения в сборку"
|
||||
"message": "Выберите файлы и папки для экспорта"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Номер версии"
|
||||
@@ -63,10 +63,10 @@
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Все данные вашей инстанции будут окончательно удалены, включая ваши миры, настройки и весь установленный контент."
|
||||
"message": "Все данные сборки будут удалены навсегда, в том числе миры, настройки и весь установленный контент."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Это действие нельзя обратить"
|
||||
"message": "Это действие необратимо"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "Удалить сборку"
|
||||
@@ -75,13 +75,13 @@
|
||||
"message": "Удаление сборки"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Эта сборка модов уже установлена в сборку <bold>{instanceName}</bold>. Вы уверены, что хотите дублировать её?"
|
||||
"message": "Выбранное содержимое уже установлено в сборке <bold>{instanceName}</bold>. Создать копию?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Создать"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Сборка модов уже установлена"
|
||||
"message": "Содержимое уже установлено"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Сборка"
|
||||
@@ -93,10 +93,10 @@
|
||||
"message": "\"{name}\" был добавлен"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "Проектов добавлено: {count}"
|
||||
"message": "{count} Проектов было добавлено"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "Посмотрите проекты, которые я использую в своей сборке модов!"
|
||||
"message": "Что в моей сборке:"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "Содержимое сборки"
|
||||
@@ -111,13 +111,13 @@
|
||||
"message": "Найти серверы"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' будет **удалён навсегда**, и будет невозможно восстановить."
|
||||
"message": "«{name}» будет **удалён навсегда**. Его невозможно восстановить."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Вы уверены, что хотите удалить этот мир навсегда?"
|
||||
"message": "Вы действительно хотите удалить этот мир?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Со сборкой"
|
||||
"message": "Модовые"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Не в сети"
|
||||
@@ -126,28 +126,28 @@
|
||||
"message": "В сети"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Не модифицировано"
|
||||
"message": "Ванильные"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "Добавить сервер или найти, чтобы начать"
|
||||
"message": "Добавьте или найдите сервер"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Сервера или мира не добавлены"
|
||||
"message": "Нет серверов и миров"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' будет удален из вашего списка, включая в игре, и восстановить будет невозможно."
|
||||
"message": "«{name}» будет удалён из списка — в том числе в игре. Его невозможно восстановить."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) будет удален из вашего списка, включая в игре, и восстановить будет невозможно."
|
||||
"message": "«{name}» ({address}) будет удалён из списка — в том числе в игре. Его невозможно восстановить."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Вы уверены, что хотите удалить {name}?"
|
||||
"message": "Вы действительно хотите удалить {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Поиск по {count, plural, one {# миру} other {# мирам}}..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "Этот сервер"
|
||||
"message": "этот сервер"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Требуется дополнительный контент"
|
||||
@@ -174,7 +174,7 @@
|
||||
"message": "Общая сборка сервера"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Посмотреть"
|
||||
"message": "Посмотреть содержимое"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Обновление перед запуском"
|
||||
@@ -261,7 +261,7 @@
|
||||
"message": "Несовместимая версия {version}"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "Ещё не играли"
|
||||
"message": "Не запускалось"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Не в сети"
|
||||
@@ -291,7 +291,7 @@
|
||||
"message": "Какое имя у друга на Modrinth?"
|
||||
},
|
||||
"friends.add-friends-to-share": {
|
||||
"message": "<link>Добавьте друзей</link>, чтобы видеть, во что они играют!"
|
||||
"message": "<link>Добавьте друзей</link> и следите за ними!"
|
||||
},
|
||||
"friends.friend.cancel-request": {
|
||||
"message": "Отменить запрос"
|
||||
@@ -330,7 +330,7 @@
|
||||
"message": "{title} — {count}"
|
||||
},
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Войдите в аккаунт Modrinth</link>, чтобы добавлять друзей и видеть, во что они играют!"
|
||||
"message": "<link>Войдите в аккаунт Modrinth</link>, чтобы добавлять друзей и следить за ними!"
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Добавить и играть"
|
||||
@@ -396,7 +396,7 @@
|
||||
"message": "Удалить сборку"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Навсегда удаляет сборку с вашего устройства, включая миры, настройки и весь установленный контент. Учтите, что после удаления сборки восстановить её будет невозможно."
|
||||
"message": "Навсегда удаляет сборку с устройства, в том числе миры, настройки и весь установленный контент. Учтите, что после удаления сборки её невозможно восстановить."
|
||||
},
|
||||
"instance.settings.tabs.general.deleting.button": {
|
||||
"message": "Удаление..."
|
||||
@@ -432,7 +432,7 @@
|
||||
"message": "Создать новую группу"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.description": {
|
||||
"message": "Разделение по группам позволяет организовать сборки по разным разделам в библиотеке."
|
||||
"message": "Позволяет организовать сборки по разным разделам в библиотеке."
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||
"message": "Введите название группы"
|
||||
@@ -591,7 +591,7 @@
|
||||
"message": "Управляется сервером"
|
||||
},
|
||||
"search.filter.locked.server-environment.title": {
|
||||
"message": "В серверную сборку можно добавлять только моды для клиента"
|
||||
"message": "В серверной сборке доступны только клиентские моды"
|
||||
},
|
||||
"search.filter.locked.server-game-version.title": {
|
||||
"message": "Версия игры управляется сервером"
|
||||
|
||||
@@ -1,16 +1,154 @@
|
||||
{
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft serveri za autentifikaciju su možda trenutno nedostupni. Molimo vas da proverite vašu internet vezu i pokušajte ponovo kasnije."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Serveri za autentifikaciju su nedostupni"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Dodaj server na instancu"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Dodaj servere na tvoju instancu"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Dodaj na instancu"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Dodaj na {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Dodato"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Već je dodato"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Otkrij sadržaj"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Otkrij servere"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Sakrije dodate servere"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Sakrije instalirani sadržaj"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instaliraj sadržaj na instancu"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Ukucaj opis modpacka..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Izvoz"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Izvezi modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Ime Modpacka"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Ime modpacka"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Izaberi fajlove i foldere da uključiš u pack"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Broj verzije"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Svi podaci za tvoju instancu će biti trajno izbrisani, uključujući tvoje svetove, konfiguracije, i sav instaliran sadržaj."
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-header": {
|
||||
"message": "Ova akcija ne može biti poništena"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "Izbriši instancu"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "Izbriši instancu"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Ovaj modpack je već instaliran u <bold>{instanceName}</bold> instancu. Da li ste sigurni da želite da je duplirate?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Napravi"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Modpack je već instaliran"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instanca"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projekat"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "\"{name}\" je dodato"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "{count} projekata je dodato"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "Pogledaj ove projekte koje ja koristim u svom modpacku!"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "Deljenje sadržaja modpacka"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Uspešno otpremljeno"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Dodaj server"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Pretraži servere"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' će biti **trajno izbrisan**, i neće biti načina da se vrati."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Da li ste sigurni da hoćete da trajno izbrišete ovaj svet?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modifikovano"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Oflajn"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "Onlajn"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Vanila"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "Dodaj server ili pretraži da bi započeo"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Nema dodatih servera niti svetova"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' će biti izbrisan sa vaše liste, uključujući i unutar igre, i neće biti načina da se oporavi."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) će biti izbrisan sa vaše liste, uključujući i unutar igre, i neće biti načina da se oporavi."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Da li ste sigurni da hoćete da izbrišete {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Pretraži {count} svetova..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "ovaj server"
|
||||
},
|
||||
"app.settings.tabs.appearance": {
|
||||
"message": "Izgled"
|
||||
},
|
||||
|
||||
@@ -111,13 +111,13 @@
|
||||
"message": "Utforska servrar"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' kommer bli **permanent borttagen**, och det kommer inte finnas något sätt att återskapa den."
|
||||
"message": "'{name}' kommer ** tas bort permanent**, och det kommer inte finnas något sätt att återskapa den."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Är du säker på att du vill permanent radera denna värld?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Moddade"
|
||||
"message": "Moddad"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Offline"
|
||||
@@ -231,7 +231,7 @@
|
||||
"message": "Nedladdning slutförd"
|
||||
},
|
||||
"app.update-popup.reload": {
|
||||
"message": "Kolla efter uppdateringar"
|
||||
"message": "Ladda om"
|
||||
},
|
||||
"app.update-popup.title": {
|
||||
"message": "Uppdatering tillgänglig"
|
||||
|
||||
@@ -2,18 +2,45 @@
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "เซิร์ฟเวอร์ตรวจสอบสิทธิ์ของ Minecraft อาจใช้งานไม่ได้ในขณะนี้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณและลองใหม่อีกครั้งในภายหลัง"
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "ไม่สามารถเชื่อมต่อถึงเซิร์ฟเวอร์ได้"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "เพิ่มเซิร์ฟเวอร์ลงในอินสแตนซ์"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "เพิ่มเซิร์ฟเวอร์ลงในอินสแตนซ์ของคุณ"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "เพิ่มลงในอินสแตนซ์"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "เพิ่มลงใน {instanceName}"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "เพิ่ม"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "เพิ่มแล้ว"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "ซ่อนเซิร์ฟเวอร์ที่เพิ่มแล้ว"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "ซ่อนเนื้อหาที่ติดตั้งแล้ว"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "ติดตั้งเนื้อหาลงในอินสแตนซ์"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "กรอกคำอธิบายมอดแพ็ก.."
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "ชื่อมอดแพ็ก"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "ชื่อ ม็อดแพ็ก"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
@@ -26,6 +53,9 @@
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "ลบอินสแตนซ์"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "ลบอินสแตนซ์"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "มอดแพ็กนี้ถูกติดตั้งไว้ในอินสแตนซ์ <bold>{instanceName}</bold> อยู่แล้ว คุณแน่ใจหรือไม่ว่าต้องการสร้างสำเนาซ้ำ?"
|
||||
},
|
||||
@@ -53,6 +83,9 @@
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "อัปโหลดสำเร็จ"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "เพิ่มเซิร์ฟเวอร์"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "ค้นหาเซิฟเวอร์"
|
||||
},
|
||||
@@ -116,6 +149,9 @@
|
||||
"app.modal.install-to-play.shared-server-instance": {
|
||||
"message": "อินสแตนซ์เซิร์ฟเวอร์ที่ใช้ร่วมกัน"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "ดูเนื้อหา"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "อัปเดตเพื่อเล่น"
|
||||
},
|
||||
@@ -149,9 +185,21 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "การจัดการทรัพยากร"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "แอฟ Modrinth v{version} พร้อมสำหรับการติดตั้งแล้ว! รีโหลดแอปเพื่ออัปเดต หรือ อัปเดตอัตโนมัติเมื่อคุณปิดแอฟ Modrinth"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "แอฟ Modrinth v{version} ดาวน์โหลดเสร็จแล้ว รีโหลดแอปเพื่ออัปเดต หรือ อัปเดตอัตโนมัติเมื่อคุณปิดแอฟ Modrinth"
|
||||
},
|
||||
"app.update-popup.body.linux": {
|
||||
"message": "Modrinth App v{version} พร้อมให้อัปเดตแล้ว โปรดใช้ตัวจัดการแพ็กเกจ ของคุณเพื่ออัปเดตฟีเจอร์ใหม่และตัวแก้ไขล่าสุด!"
|
||||
},
|
||||
"app.update-popup.body.metered": {
|
||||
"message": "แอฟ Modrinth v{version} พร้อมแล้ว! เนื่องจากคุณเปิดโหมดจำกัดปริมาณข้อมูล เราจึงไม่ได้อัปเดตอัตโนมัติ"
|
||||
},
|
||||
"app.update-popup.download": {
|
||||
"message": "ดาวโหลด ({size})"
|
||||
},
|
||||
"app.update-popup.download-complete": {
|
||||
"message": "ดาวน์โหลดเสร็จสมบูรณ์"
|
||||
},
|
||||
@@ -176,6 +224,9 @@
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "เลือกตัวเลือก"
|
||||
},
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "เวอร์ชัน {version} ไม่รองรับ"
|
||||
},
|
||||
@@ -473,6 +524,9 @@
|
||||
"search.filter.locked.instance": {
|
||||
"message": "อินสแตนซ์เป็นผู้จัดเตรียม"
|
||||
},
|
||||
"search.filter.locked.instance.sync": {
|
||||
"message": "ซิงค์กับอินสแตนซ์"
|
||||
},
|
||||
"search.filter.locked.server": {
|
||||
"message": "เซิร์ฟเวอร์เป็นผู้จัดเตรียม"
|
||||
},
|
||||
|
||||
@@ -6,49 +6,49 @@
|
||||
"message": "Doğrulama sunucularına erişilemedi"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Sunucuya içerik ekle"
|
||||
"message": "Kurulumunuza sunucu ekleyin"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "İçeriğe sunucu ekle"
|
||||
"message": "Kurulumunuza sunucular ekleyin"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "İçerik ekle"
|
||||
"message": "Kuruluma ekle"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "{instanceName} içeriğine ekle"
|
||||
"message": "{instanceName}'a ekle"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "Eklendi"
|
||||
"message": "Ekli"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Zaten eklendi"
|
||||
"message": "Zaten ekli"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "İçerikleri keşfet"
|
||||
"message": "İçerik keşfet"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Sunucuları keşfet"
|
||||
"message": "Sunucu keşfet"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Eklenen sunucuları gizle\n"
|
||||
"message": "Ekli sunucuları gizle"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Yüklü İçerikleri Gizle"
|
||||
"message": "Kurulu içerikleri gizle"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "İçeriği profile yükle"
|
||||
"message": "İçeriği kuruluma yükle"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Mod paketi açıklaması yazın..."
|
||||
"message": "Mod paketi açıklaması girin..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Çıkart"
|
||||
"message": "Dışa aktar"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Modpaketi çıkart"
|
||||
"message": "Mod paketini dışa aktar"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpaketi adı"
|
||||
"message": "Mod Paketi Adı"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpaketi adı"
|
||||
@@ -69,22 +69,22 @@
|
||||
"message": "Bu eylem geri alınamaz"
|
||||
},
|
||||
"app.instance.confirm-delete.delete-button": {
|
||||
"message": "İçeriği Sil"
|
||||
"message": "Kurulumu sil"
|
||||
},
|
||||
"app.instance.confirm-delete.header": {
|
||||
"message": "İçeriği Sil"
|
||||
"message": "Kurulumu sil"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Bu mod paketi zaten <bold>{instanceName}</bold> örneğinde yüklü. Kopyalamak istediğinizden emin misiniz?"
|
||||
"message": "Bu mod paketi zaten <bold>{instanceName}</bold> kurulumunda yüklü. Kopyalamak istediğinizden emin misiniz?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Oluştur"
|
||||
},
|
||||
"app.instance.modpack-already-installed.header": {
|
||||
"message": "Modpaketi zaten kurulu"
|
||||
"message": "Modpaketi zaten yüklü"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "İçerik"
|
||||
"message": "Kurulum"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "proje"
|
||||
@@ -102,16 +102,16 @@
|
||||
"message": "Mod paketi içeriğini paylaşıyorsunuz"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Başarı ile yüklendi"
|
||||
"message": "Başarıyla yüklendi"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Sunucu Ekle"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Sunucuya göz at"
|
||||
"message": "Sunucuları Keşfet"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' kalıcı olarak silinecek ve geri getirilmesi mümkün olmayacaktır."
|
||||
"message": "'{name}' **kalıcı olarak silinecek** ve geri getirilmesi mümkün olmayacaktır."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Bu dünyayı kalıcı olarak silmek istediğinizden emin misiniz?"
|
||||
@@ -120,10 +120,10 @@
|
||||
"message": "Modlu"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "Çevrimdışı"
|
||||
"message": "Çevrim dışı"
|
||||
},
|
||||
"app.instance.worlds.filter-online": {
|
||||
"message": "Çevrimiçi"
|
||||
"message": "Çevrim içi"
|
||||
},
|
||||
"app.instance.worlds.filter-vanilla": {
|
||||
"message": "Vanilla"
|
||||
@@ -141,7 +141,7 @@
|
||||
"message": "'{name}' ({address}) oyun içi dahil olmak üzere listenizden kaldırılacak ve geri getirilmesi mümkün olmayacaktır."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "{name} öğesini kaldırmak istediğinizden emin misiniz?"
|
||||
"message": "{name}'i kaldırmak istediğinizden emin misiniz?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "{count} dünya ara..."
|
||||
@@ -153,7 +153,7 @@
|
||||
"message": "İçerik gerekli"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Yükleyip oynayın"
|
||||
"message": "Oynamak için yükleyin"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Yükle"
|
||||
@@ -210,19 +210,19 @@
|
||||
"message": "Kaynak yönetimi"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App v{version} güncellemesi hazır! Hemen güncellemek için yeniden yükle veya Modrinth App’i kapattığında otomatik olarak güncellenecek."
|
||||
"message": "Modrinth App v{version} güncellemesi indirilmeye hazır! Hemen güncellemek için yeniden başlatın veya Modrinth App’i kapattığınızda otomatik olarak güncellenecek."
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} indirildi. Güncellemek için sayfayı yeniden yükle veya Modrinth App’i kapattığında otomatik olarak güncellenecek."
|
||||
"message": "Modrinth App v{version} indirildi. Güncellemek için yeniden başlatın veya Modrinth App’i kapattığınızda otomatik olarak güncellenecek."
|
||||
},
|
||||
"app.update-popup.body.linux": {
|
||||
"message": "Modrinth App v{version} yayımlandı. En yeni özellikler ve hata düzeltmeleri için paket yöneticin üzerinden güncelle!"
|
||||
},
|
||||
"app.update-popup.body.metered": {
|
||||
"message": "Modrinth App v{version} artık mevcut! Ölçüllü bağlantıda olduğunuzdan otomatik olarak indirmedik."
|
||||
"message": "Modrinth App v{version} mevcut! Kısıtlı bağlantıda olduğunuzdan otomatik olarak indirmedik."
|
||||
},
|
||||
"app.update-popup.changelog": {
|
||||
"message": "Değişiklikler"
|
||||
"message": "Yama Notları"
|
||||
},
|
||||
"app.update-popup.download": {
|
||||
"message": "İndir ({size})"
|
||||
@@ -231,7 +231,7 @@
|
||||
"message": "İndirme tamamlandı"
|
||||
},
|
||||
"app.update-popup.reload": {
|
||||
"message": "Yeniden yükle"
|
||||
"message": "Yeniden başlat"
|
||||
},
|
||||
"app.update-popup.title": {
|
||||
"message": "Güncelleme mevcut"
|
||||
@@ -249,7 +249,7 @@
|
||||
"message": "Güncelleme indiriliyor (%{percent})"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Güncellemeyi kurmak için yeniden yükleyin"
|
||||
"message": "Güncellemeyi kurmak için yeniden başlatın"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
@@ -264,7 +264,7 @@
|
||||
"message": "Henüz oynanmadı"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Çevrimdışı"
|
||||
"message": "Çevrim dışı"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} çevrimiçi"
|
||||
@@ -390,10 +390,10 @@
|
||||
"message": "Genel"
|
||||
},
|
||||
"instance.settings.tabs.general.delete": {
|
||||
"message": "Profili sil"
|
||||
"message": "Kurulumu sil"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.button": {
|
||||
"message": "Profili sil"
|
||||
"message": "Kurulumu sil"
|
||||
},
|
||||
"instance.settings.tabs.general.delete.description": {
|
||||
"message": "Profili, içindeki dünyaları, ayarları ve indirilen şeyleri cihazınızdan sonsuza kadar siler. Dikkatli olun, bir profil silindikten sonra geri alınamaz."
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"message": "Дослідити сервера"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Сховати додані сервера"
|
||||
"message": "Сховати додані сервери"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Сховати встановлений уміст"
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
"message": "无法连接到身份验证服务器"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "将服务器添加到实例"
|
||||
"message": "将服务器添加到实例实例"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "将服务器添加到你的实例"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "添加到实例"
|
||||
"message": "添加到实例实例"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "添加到 {instanceName}"
|
||||
@@ -21,7 +21,7 @@
|
||||
"message": "已添加"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "已添加过"
|
||||
"message": "已添加"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "发现内容"
|
||||
@@ -33,7 +33,7 @@
|
||||
"message": "隐藏已添加的服务器"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "隐藏已安装的实例"
|
||||
"message": "隐藏已安装的内容"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "将内容安装到实例中"
|
||||
@@ -90,13 +90,13 @@
|
||||
"message": "项目"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "已添加“{name}”"
|
||||
"message": "已添加 “{name}”"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "已添加 {count} 个项目"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "查看在我整合包中使用的项目!"
|
||||
"message": "查看我在整合包中使用的项目!"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "分享整合包内容"
|
||||
@@ -117,7 +117,7 @@
|
||||
"message": "你确定要永久删除这个世界吗?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "修改版"
|
||||
"message": "模组适配"
|
||||
},
|
||||
"app.instance.worlds.filter-offline": {
|
||||
"message": "离线"
|
||||
@@ -129,7 +129,7 @@
|
||||
"message": "原版"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-description": {
|
||||
"message": "添加一个服务器或点击浏览来开始"
|
||||
"message": "通过添加一个服务器或点击浏览以开始"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "未添加服务器或世界"
|
||||
@@ -150,7 +150,7 @@
|
||||
"message": "此服务器"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "内容需求"
|
||||
"message": "需求内容"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "安装以游玩"
|
||||
@@ -162,7 +162,7 @@
|
||||
"message": "{count} 个模组"
|
||||
},
|
||||
"app.modal.install-to-play.required-modpack": {
|
||||
"message": "整合包需求"
|
||||
"message": "需求整合包"
|
||||
},
|
||||
"app.modal.install-to-play.server-requires-mods": {
|
||||
"message": "此服务器需要安装模组才能游玩。点击安装,从 Modrinth 下载所需文件,然后直接进入服务器。"
|
||||
@@ -441,7 +441,7 @@
|
||||
"message": "名称"
|
||||
},
|
||||
"instance.settings.tabs.hooks": {
|
||||
"message": "启动Hooks"
|
||||
"message": "启动 Hooks"
|
||||
},
|
||||
"instance.settings.tabs.hooks.custom-hooks": {
|
||||
"message": "自定义启动Hooks"
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
"message": "無法連線到驗證伺服器"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "新增此伺服器到實例中"
|
||||
"message": "將伺服器新增到實例"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "新增伺服器到實例中"
|
||||
"message": "將伺服器新增到實例"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "新增至實例"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "新增到 {instanceName}"
|
||||
"message": "新增至「{instanceName}」"
|
||||
},
|
||||
"app.browse.added": {
|
||||
"message": "已新增"
|
||||
@@ -216,7 +216,7 @@
|
||||
"message": "Modrinth App v{version} 已完成下載!立即重新載入以更新,或在關閉 Modrinth App 時自動更新。"
|
||||
},
|
||||
"app.update-popup.body.linux": {
|
||||
"message": "Modrinth App v{version} 現已推出。請使用套件管理員進行更新,以取得最新的功能與修正!"
|
||||
"message": "Modrinth App v{version} 現已推出。請使用軟體包管理員進行更新,以取得最新的功能與修正!"
|
||||
},
|
||||
"app.update-popup.body.metered": {
|
||||
"message": "Modrinth App v{version} 現已推出!由於你目前使用的是計量付費網路,我們並未自動下載更新。"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'floating-vue/dist/style.css'
|
||||
import 'overlayscrollbars/overlayscrollbars.css'
|
||||
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
||||
@@ -8,6 +9,7 @@ import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from '@/App.vue'
|
||||
import { overlayScrollbarsDirective } from '@/directives/overlayScrollbars'
|
||||
import i18nPlugin from '@/plugins/i18n'
|
||||
import i18nDebugPlugin from '@/plugins/i18n-debug'
|
||||
import router from '@/routes'
|
||||
@@ -50,5 +52,6 @@ app.use(FloatingVue, {
|
||||
})
|
||||
app.use(i18nPlugin)
|
||||
app.use(i18nDebugPlugin)
|
||||
app.directive('overlay-scrollbars', overlayScrollbarsDirective)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { injectModrinthClient, ServersManagePageIndex } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { config } from '../config'
|
||||
|
||||
const stripePublishableKey = (config.stripePublishableKey as string) || ''
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
const { data: products } = useQuery({
|
||||
queryKey: ['billing', 'products'],
|
||||
queryFn: () => client.labrinth.billing_internal.getProducts(),
|
||||
})
|
||||
|
||||
const resolvedProducts = computed<Labrinth.Billing.Internal.Product[]>(() => products.value ?? [])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManagePageIndex
|
||||
:stripe-publishable-key="stripePublishableKey"
|
||||
:products="resolvedProducts"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
ServersManageBackupsPage,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, worldId, isServerRunning } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
if (worldId.value) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['backups', 'list', serverId],
|
||||
queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManageBackupsPage :is-server-running="isServerRunning" />
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
ServersManageContentPage,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { serverId, worldId } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
if (worldId.value) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['content', 'list', 'v1', serverId],
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManageContentPage />
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
injectModrinthClient,
|
||||
injectModrinthServerContext,
|
||||
ServersManageFilesPage,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { serverId } = injectModrinthServerContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['files', serverId, '/'],
|
||||
queryFn: () => client.kyros.files_v0.listDirectory('/', 1, 2000),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManageFilesPage />
|
||||
</template>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="h-full w-full pt-6">
|
||||
<ServersManageRootLayout
|
||||
:server-id="serverId"
|
||||
:reload-page="() => router.go(0)"
|
||||
:resolve-viewer="resolveViewer"
|
||||
:show-copy-id-action="themeStore.devMode"
|
||||
:auth-user="authUser"
|
||||
:navigate-to-billing="() => openUrl('https://modrinth.com/settings/billing')"
|
||||
:navigate-to-servers="() => router.push('/hosting/manage')"
|
||||
:browse-modpacks="
|
||||
({ serverId: sid, worldId: wid, from }) => {
|
||||
router.push({
|
||||
path: '/browse/modpack',
|
||||
query: { sid, wid: wid ?? undefined, from },
|
||||
})
|
||||
}
|
||||
"
|
||||
:browse-content="
|
||||
({ serverId: sid, worldId: wid, type }) => {
|
||||
router.push({
|
||||
path: `/browse/${type}`,
|
||||
query: { sid, wid: wid ?? undefined },
|
||||
})
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #default="{ onReinstall, onReinstallFailed }">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<template v-if="Component">
|
||||
<Suspense>
|
||||
<component
|
||||
:is="Component"
|
||||
@reinstall="onReinstall"
|
||||
@reinstall-failed="onReinstallFailed"
|
||||
/>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
</template>
|
||||
</ServersManageRootLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import { injectAuth, injectModrinthClient, ServersManageRootLayout } from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { get as getCreds } from '@/helpers/mr_auth'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const themeStore = useTheming()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const serverId = computed(() => {
|
||||
const rawId = route.params.id
|
||||
return Array.isArray(rawId) ? rawId[0] : (rawId ?? '')
|
||||
})
|
||||
|
||||
if (serverId.value) {
|
||||
try {
|
||||
await queryClient.ensureQueryData({
|
||||
queryKey: ['servers', 'detail', serverId.value],
|
||||
queryFn: () => client.archon.servers_v0.get(serverId.value)!,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
} catch {
|
||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||
}
|
||||
}
|
||||
|
||||
const { data: serverData } = useQuery({
|
||||
queryKey: computed(() => ['servers', 'detail', serverId.value]),
|
||||
queryFn: () => null as unknown as Archon.Servers.v0.Server,
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
watch(
|
||||
serverData,
|
||||
(server) => {
|
||||
if (server?.name) {
|
||||
breadcrumbs.setName('Server', server.name)
|
||||
breadcrumbs.setContext({
|
||||
name: server.name,
|
||||
link: `/hosting/manage/${serverId.value}/content`,
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => auth.user.value,
|
||||
(user, previousUser) => {
|
||||
if (user || !previousUser) return
|
||||
if (route.path === '/hosting/manage' || route.path === '/hosting/manage/') return
|
||||
void router.replace('/hosting/manage')
|
||||
},
|
||||
)
|
||||
|
||||
const authUser = computed(() => {
|
||||
const user = auth.user.value
|
||||
if (!user?.id) return undefined
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email ?? '',
|
||||
created: user.created,
|
||||
}
|
||||
})
|
||||
|
||||
async function resolveViewer(): Promise<{ userId: string | null; userRole: string | null }> {
|
||||
const credentials = await getCreds().catch(() => null)
|
||||
if (!credentials?.user_id) {
|
||||
return { userId: null, userRole: null }
|
||||
}
|
||||
|
||||
const user = await get_user(credentials.user_id, 'bypass').catch(() => null)
|
||||
const typedUser = user as Labrinth.Users.v2.User | null
|
||||
return {
|
||||
userId: credentials.user_id,
|
||||
userRole: typedUser?.role ?? null,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ServersManageOverviewPage } from '@modrinth/ui'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ServersManageOverviewPage />
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
import Backups from './Backups.vue'
|
||||
import Content from './Content.vue'
|
||||
import Files from './Files.vue'
|
||||
import Index from './Index.vue'
|
||||
import Overview from './Overview.vue'
|
||||
|
||||
export { Backups, Content, Files, Index, Overview }
|
||||
@@ -1,6 +1,7 @@
|
||||
import Browse from './Browse.vue'
|
||||
import Index from './Index.vue'
|
||||
import Servers from './Servers.vue'
|
||||
import Skins from './Skins.vue'
|
||||
import Worlds from './Worlds.vue'
|
||||
|
||||
export { Browse, Index, Skins, Worlds }
|
||||
export { Browse, Index, Servers, Skins, Worlds }
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
FilePageLayout,
|
||||
injectNotificationManager,
|
||||
provideFileManager,
|
||||
ReadyTransition,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
@@ -54,6 +55,8 @@ const messages = defineMessages({
|
||||
|
||||
const instanceRoot = ref('')
|
||||
const items = ref<FileItem[]>([])
|
||||
/** True until the first directory read for the current instance path finishes (initial load only). */
|
||||
const firstPaintPending = ref(true)
|
||||
const loading = ref(true)
|
||||
const error = ref<Error | null>(null)
|
||||
const currentPath = ref('')
|
||||
@@ -123,6 +126,7 @@ async function refresh() {
|
||||
items.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
firstPaintPending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +309,7 @@ watch(
|
||||
() => props.instance.path,
|
||||
async () => {
|
||||
debug('watch instance.path: changed to', props.instance.path)
|
||||
firstPaintPending.value = true
|
||||
instanceRoot.value = await get_full_path(props.instance.path)
|
||||
currentPath.value = ''
|
||||
await refresh()
|
||||
@@ -341,5 +346,7 @@ provideFileManager({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FilePageLayout :show-refresh-button="true" />
|
||||
<ReadyTransition :pending="firstPaintPending">
|
||||
<FilePageLayout :show-refresh-button="true" />
|
||||
</ReadyTransition>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div v-if="instance">
|
||||
<div class="p-6 pr-2 pb-4" @contextmenu.prevent.stop="(event) => handleRightClick(event)">
|
||||
<div v-if="instance" :class="{ 'flex h-full flex-col': isFixedRender }">
|
||||
<div
|
||||
:class="['p-6 pr-2 pb-4', { 'shrink-0': isFixedRender }]"
|
||||
@contextmenu.prevent.stop="(event) => handleRightClick(event)"
|
||||
>
|
||||
<ExportModal ref="exportModal" :instance="instance" />
|
||||
<InstanceSettingsModal
|
||||
:key="instance.path"
|
||||
@@ -114,9 +117,9 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing === true" color="red" size="large">
|
||||
<button @click="stopInstance('InstancePage')">
|
||||
<button :disabled="stopping" @click="stopInstance('InstancePage')">
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
{{ stopping ? 'Stopping...' : 'Stop' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
@@ -172,7 +175,7 @@
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
<button disabled>Loading...</button>
|
||||
<button disabled>Starting...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large">
|
||||
<button v-tooltip="'Instance settings'" @click="settingsModal?.show()">
|
||||
@@ -205,21 +208,17 @@
|
||||
</template>
|
||||
</ContentPageHeader>
|
||||
</div>
|
||||
<div class="px-6">
|
||||
<div :class="['px-6', { 'shrink-0': isFixedRender }]">
|
||||
<NavTabs :links="tabs" />
|
||||
</div>
|
||||
<div v-if="!!instance" class="p-6 pt-4">
|
||||
<div :class="['p-6 pt-4', { 'min-h-0 flex-1 overflow-y-auto': isFixedRender }]">
|
||||
<RouterView
|
||||
v-if="route.path.startsWith('/instance')"
|
||||
v-slot="{ Component }"
|
||||
:key="instance.path"
|
||||
>
|
||||
<template v-if="Component">
|
||||
<Suspense
|
||||
:key="instance.path"
|
||||
@pending="loadingBar.startLoading()"
|
||||
@resolve="loadingBar.stopLoading()"
|
||||
>
|
||||
<Suspense :key="instance.path">
|
||||
<component
|
||||
:is="Component"
|
||||
:instance="instance"
|
||||
@@ -232,9 +231,6 @@
|
||||
@play="updatePlayState"
|
||||
@stop="() => stopInstance('InstanceSubpage')"
|
||||
></component>
|
||||
<template #fallback>
|
||||
<LoadingIndicator />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
</RouterView>
|
||||
@@ -293,7 +289,6 @@ import {
|
||||
ButtonStyled,
|
||||
ContentPageHeader,
|
||||
injectNotificationManager,
|
||||
LoadingIndicator,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
ServerOnlinePlayers,
|
||||
@@ -301,6 +296,7 @@ import {
|
||||
ServerRecentPlays,
|
||||
ServerRegion,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import duration from 'dayjs/plugin/duration'
|
||||
@@ -312,6 +308,7 @@ import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_v3 } from '@/helpers/cache.js'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
@@ -319,16 +316,17 @@ import { get_by_profile_path } from '@/helpers/process'
|
||||
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { get_server_status } from '@/helpers/worlds'
|
||||
import { get_server_status, refreshWorlds } from '@/helpers/worlds'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { useBreadcrumbs, useLoading } from '@/store/state'
|
||||
import { useBreadcrumbs } from '@/store/state'
|
||||
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { playServerProject } = injectServerInstall()
|
||||
const queryClient = useQueryClient()
|
||||
const route = useRoute()
|
||||
|
||||
const router = useRouter()
|
||||
@@ -345,6 +343,7 @@ window.addEventListener('online', () => {
|
||||
const instance = ref<GameInstance>()
|
||||
const playing = ref(false)
|
||||
const loading = ref(false)
|
||||
const stopping = ref(false)
|
||||
const exportModal = ref<InstanceType<typeof ExportModal>>()
|
||||
const updateToPlayModal = ref<InstanceType<typeof UpdateToPlayModal>>()
|
||||
|
||||
@@ -387,6 +386,14 @@ async function fetchInstance() {
|
||||
}
|
||||
|
||||
fetchDeferredData()
|
||||
|
||||
if (instance.value) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['worlds', instance.value.path],
|
||||
queryFn: () => refreshWorlds(instance.value!.path),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDeferredData() {
|
||||
@@ -429,6 +436,19 @@ watch(
|
||||
|
||||
const basePath = computed(() => `/instance/${encodeURIComponent(route.params.id as string)}`)
|
||||
|
||||
/**
|
||||
* Per-route layout mode.
|
||||
* - `'scroll'` (default): the whole instance page scrolls inside `.app-viewport`. This lets
|
||||
* `position: sticky` children (and the viewport-rooted `IntersectionObserver` used by
|
||||
* `useStickyObserver`) work correctly.
|
||||
* - `'fixed'`: the header + tabs are pinned and only the tab body scrolls in its own container.
|
||||
* Used by tabs whose content (e.g. the log console) needs a bounded height to resolve `h-full`.
|
||||
*/
|
||||
const renderMode = computed<'scroll' | 'fixed'>(() =>
|
||||
route.meta.renderMode === 'fixed' ? 'fixed' : 'scroll',
|
||||
)
|
||||
const isFixedRender = computed(() => renderMode.value === 'fixed')
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
label: 'Content',
|
||||
@@ -466,8 +486,6 @@ if (instance.value) {
|
||||
})
|
||||
}
|
||||
|
||||
const loadingBar = useLoading()
|
||||
|
||||
const options = ref<InstanceType<typeof ContextMenu> | null>(null)
|
||||
|
||||
const startInstance = async (context: string) => {
|
||||
@@ -494,8 +512,10 @@ const startInstance = async (context: string) => {
|
||||
}
|
||||
|
||||
const stopInstance = async (context: string) => {
|
||||
playing.value = false
|
||||
stopping.value = true
|
||||
await kill(route.params.id as string).catch(handleError)
|
||||
stopping.value = false
|
||||
playing.value = false
|
||||
|
||||
if (!instance.value) return
|
||||
trackEvent('InstanceStop', {
|
||||
@@ -644,6 +664,11 @@ const timePlayedHumanized = computed(() => {
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenProfiles()
|
||||
const profilePath = route.params.id
|
||||
if (profilePath) {
|
||||
const { destroy } = useInstanceConsole(profilePath)
|
||||
destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,127 +1,24 @@
|
||||
<template>
|
||||
<Card class="log-card">
|
||||
<div class="button-row">
|
||||
<DropdownSelect
|
||||
v-model="selectedLogIndex"
|
||||
:default-value="0"
|
||||
name="Log date"
|
||||
:options="logs.map((_, index) => index)"
|
||||
:display-name="(option) => logs[option]?.name"
|
||||
:disabled="logs.length === 0"
|
||||
/>
|
||||
<div class="button-group">
|
||||
<Button :disabled="!logs[selectedLogIndex]" @click="copyLog()">
|
||||
<ClipboardCopyIcon v-if="!copied" />
|
||||
<CheckIcon v-else />
|
||||
{{ copied ? 'Copied' : 'Copy' }}
|
||||
</Button>
|
||||
<Button color="primary" :disabled="offline || !logs[selectedLogIndex]" @click="share">
|
||||
<ShareIcon aria-hidden="true" />
|
||||
Share
|
||||
</Button>
|
||||
<Button
|
||||
v-if="logs[selectedLogIndex] && logs[selectedLogIndex].live === true"
|
||||
@click="clearLiveLog()"
|
||||
>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Clear
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-else
|
||||
:disabled="!logs[selectedLogIndex] || logs[selectedLogIndex].live === true"
|
||||
color="danger"
|
||||
@click="deleteLog()"
|
||||
>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<StyledInput
|
||||
id="text-filter"
|
||||
v-model="searchFilter"
|
||||
autocomplete="off"
|
||||
:icon="SearchIcon"
|
||||
type="search"
|
||||
input-class="text-filter"
|
||||
placeholder="Type to filter logs..."
|
||||
/>
|
||||
<div class="filter-group">
|
||||
<Checkbox
|
||||
v-for="level in levels"
|
||||
:key="level.toLowerCase()"
|
||||
v-model="levelFilters[level.toLowerCase()]"
|
||||
class="filter-checkbox"
|
||||
>
|
||||
{{ level }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-text">
|
||||
<RecycleScroller
|
||||
v-slot="{ item }"
|
||||
ref="logContainer"
|
||||
class="scroller"
|
||||
:items="displayProcessedLogs"
|
||||
direction="vertical"
|
||||
:item-size="20"
|
||||
key-field="id"
|
||||
buffer="200"
|
||||
>
|
||||
<div class="user no-wrap">
|
||||
<span :style="{ color: item.prefixColor, 'font-weight': item.weight }">{{
|
||||
item.prefix
|
||||
}}</span>
|
||||
<span :style="{ color: item.textColor }">{{ item.text }}</span>
|
||||
</div>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
header="Share Log"
|
||||
share-title="Instance Log"
|
||||
share-text="Check out this log from an instance on the Modrinth App"
|
||||
:open-in-new-tab="false"
|
||||
link
|
||||
/>
|
||||
</Card>
|
||||
<div class="flex flex-col gap-4 h-full">
|
||||
<ConsolePageLayout />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { CheckIcon, ClipboardCopyIcon, SearchIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
DropdownSelect,
|
||||
ConsolePageLayout,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
provideConsoleManager,
|
||||
} from '@modrinth/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import isToday from 'dayjs/plugin/isToday'
|
||||
import isYesterday from 'dayjs/plugin/isYesterday'
|
||||
import { ofetch } from 'ofetch'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, shallowRef, triggerRef, watch, watchEffect } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { process_listener } from '@/helpers/events.js'
|
||||
import {
|
||||
delete_logs_by_filename,
|
||||
get_latest_log_cursor,
|
||||
get_logs,
|
||||
get_output_by_filename,
|
||||
} from '@/helpers/logs.js'
|
||||
import { get_by_profile_path } from '@/helpers/process.js'
|
||||
|
||||
dayjs.extend(isToday)
|
||||
dayjs.extend(isYesterday)
|
||||
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
||||
import { log_listener, process_listener } from '@/helpers/events.js'
|
||||
import { delete_logs_by_filename, get_output_by_filename } from '@/helpers/logs.js'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -158,414 +55,181 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const currentLiveLog = ref(null)
|
||||
const currentLiveLogCursor = ref(0)
|
||||
const emptyText = ['No live game detected.', 'Start your game to proceed.']
|
||||
const profilePathId = computed(() => route.params.id)
|
||||
const {
|
||||
liveConsole,
|
||||
historicalConsole,
|
||||
hydrate,
|
||||
getHistoricalLogs,
|
||||
getHistoricalContent,
|
||||
invalidate,
|
||||
clearLive,
|
||||
} = useInstanceConsole(profilePathId.value)
|
||||
|
||||
const logs = ref([])
|
||||
await setLogs()
|
||||
await hydrate()
|
||||
|
||||
const logsColored = true
|
||||
function buildLogList(rawLogs) {
|
||||
return [
|
||||
{ name: 'Live Log', live: true },
|
||||
...rawLogs
|
||||
.filter(
|
||||
(log) =>
|
||||
log.filename !== 'latest_stdout.log' &&
|
||||
log.filename !== 'latest_stdout' &&
|
||||
log.filename !== 'launcher_log.txt' &&
|
||||
log.stdout !== '' &&
|
||||
(log.filename.includes('.log') || log.filename.endsWith('.txt')),
|
||||
)
|
||||
.map((log) => ({
|
||||
...log,
|
||||
name: log.filename || 'Unknown',
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
const logs = ref(buildLogList([]))
|
||||
|
||||
void getHistoricalLogs(props.instance.path)
|
||||
.then((allLogs) => {
|
||||
logs.value = buildLogList(allLogs)
|
||||
})
|
||||
.catch(handleError)
|
||||
|
||||
const selectedLogIndex = ref(0)
|
||||
const copied = ref(false)
|
||||
const logContainer = ref(null)
|
||||
const interval = ref(null)
|
||||
const userScrolled = ref(false)
|
||||
const isAutoScrolling = ref(false)
|
||||
const shareModal = ref(null)
|
||||
const isLive = computed(() => selectedLogIndex.value === 0)
|
||||
|
||||
const levels = ['Comment', 'Error', 'Warn', 'Info', 'Debug', 'Trace']
|
||||
const levelFilters = ref({})
|
||||
levels.forEach((level) => {
|
||||
levelFilters.value[level.toLowerCase()] = true
|
||||
const filteredLogs = computed(() =>
|
||||
props.playing ? logs.value.filter((l) => l.live || l.name !== 'latest.log') : logs.value,
|
||||
)
|
||||
|
||||
const logSources = computed(() =>
|
||||
filteredLogs.value.map((l, i) => ({
|
||||
id: String(i),
|
||||
name: l?.name ?? `Log ${i}`,
|
||||
live: l?.live ?? false,
|
||||
})),
|
||||
)
|
||||
|
||||
const activeConsole = computed(() => (isLive.value ? liveConsole : historicalConsole))
|
||||
|
||||
const logLines = shallowRef(activeConsole.value.output.value)
|
||||
watchEffect(() => {
|
||||
logLines.value = activeConsole.value.output.value
|
||||
triggerRef(logLines)
|
||||
})
|
||||
const searchFilter = ref('')
|
||||
|
||||
function shouldDisplay(processedLine) {
|
||||
if (!processedLine.level) {
|
||||
return true
|
||||
}
|
||||
const crashAnalysis = ref(null)
|
||||
|
||||
if (!levelFilters.value[processedLine.level.toLowerCase()]) {
|
||||
return false
|
||||
}
|
||||
if (searchFilter.value !== '') {
|
||||
if (!processedLine.text.toLowerCase().includes(searchFilter.value.toLowerCase())) {
|
||||
return false
|
||||
async function analyseForCrash() {
|
||||
const lines = liveConsole.output.value
|
||||
if (lines.length === 0) return
|
||||
|
||||
const content = lines.map((l) => l.text).join('\n')
|
||||
try {
|
||||
const data = await client.mclogs.insights_v1.analyse(content)
|
||||
if (data.analysis?.problems?.length > 0) {
|
||||
crashAnalysis.value = data
|
||||
}
|
||||
} catch {
|
||||
// Crash analysis is best-effort
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Selects from the processed logs which ones should be displayed (shouldDisplay)
|
||||
// In addition, splits each line by \n. Each split line is given the same properties as the original line
|
||||
const displayProcessedLogs = computed(() => {
|
||||
return processedLogs.value.filter((l) => shouldDisplay(l))
|
||||
const selectedLog = computed(() => filteredLogs.value[selectedLogIndex.value])
|
||||
|
||||
const deleteDisabled = computed(() => {
|
||||
const log = selectedLog.value
|
||||
if (!log || log.live) return true
|
||||
return log.filename === 'latest.log' && props.playing
|
||||
})
|
||||
|
||||
const processedLogs = computed(() => {
|
||||
// split based on newline and timestamp lookahead
|
||||
// (not just newline because of multiline messages)
|
||||
const splitPattern = /\n(?=(?:#|\[\d\d:\d\d:\d\d\]))/
|
||||
|
||||
const lines = logs.value[selectedLogIndex.value]?.stdout.split(splitPattern) || []
|
||||
const processed = []
|
||||
let id = 0
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
// Then split off of \n.
|
||||
// Lines that are not the first have prefix = null
|
||||
const text = getLineText(lines[i])
|
||||
const prefix = getLinePrefix(lines[i])
|
||||
const prefixColor = getLineColor(lines[i], true)
|
||||
const textColor = getLineColor(lines[i], false)
|
||||
const weight = getLineWeight(lines[i])
|
||||
const level = getLineLevel(lines[i])
|
||||
text.split('\n').forEach((line, index) => {
|
||||
processed.push({
|
||||
id: id,
|
||||
text: line,
|
||||
prefix: index === 0 ? prefix : null,
|
||||
prefixColor: prefixColor,
|
||||
textColor: textColor,
|
||||
weight: weight,
|
||||
level: level,
|
||||
})
|
||||
id += 1
|
||||
})
|
||||
}
|
||||
return processed
|
||||
})
|
||||
|
||||
async function getLiveStdLog() {
|
||||
if (route.params.id) {
|
||||
const processes = await get_by_profile_path(route.params.id).catch(handleError)
|
||||
let returnValue
|
||||
if (processes.length === 0) {
|
||||
returnValue = emptyText.join('\n')
|
||||
} else {
|
||||
const logCursor = await get_latest_log_cursor(
|
||||
props.instance.path,
|
||||
currentLiveLogCursor.value,
|
||||
).catch(handleError)
|
||||
if (logCursor.new_file) {
|
||||
currentLiveLog.value = ''
|
||||
}
|
||||
currentLiveLog.value = currentLiveLog.value + logCursor.output
|
||||
currentLiveLogCursor.value = logCursor.cursor
|
||||
returnValue = currentLiveLog.value
|
||||
}
|
||||
return { name: 'Live Log', stdout: returnValue, live: true }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function getLogs() {
|
||||
return (await get_logs(props.instance.path, true).catch(handleError))
|
||||
.filter(
|
||||
// filter out latest_stdout.log or anything without .log in it
|
||||
(log) =>
|
||||
log.filename !== 'latest_stdout.log' &&
|
||||
log.filename !== 'latest_stdout' &&
|
||||
log.stdout !== '' &&
|
||||
(log.filename.includes('.log') || log.filename.endsWith('.txt')),
|
||||
)
|
||||
.map((log) => {
|
||||
log.name = log.filename || 'Unknown'
|
||||
log.stdout = 'Loading...'
|
||||
return log
|
||||
})
|
||||
}
|
||||
|
||||
async function setLogs() {
|
||||
const [liveStd, allLogs] = await Promise.all([getLiveStdLog(), getLogs()])
|
||||
logs.value = [liveStd, ...allLogs]
|
||||
}
|
||||
|
||||
const copyLog = () => {
|
||||
if (logs.value.length > 0 && logs.value[selectedLogIndex.value]) {
|
||||
navigator.clipboard.writeText(logs.value[selectedLogIndex.value].stdout)
|
||||
copied.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const share = async () => {
|
||||
if (logs.value.length > 0 && logs.value[selectedLogIndex.value]) {
|
||||
const url = await ofetch('https://api.mclo.gs/1/log', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `content=${encodeURIComponent(logs.value[selectedLogIndex.value].stdout)}`,
|
||||
}).catch(handleError)
|
||||
|
||||
shareModal.value.show(url.url)
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedLogIndex, async (newIndex) => {
|
||||
copied.value = false
|
||||
userScrolled.value = false
|
||||
|
||||
if (logs.value.length > 1 && newIndex !== 0) {
|
||||
logs.value[newIndex].stdout = 'Loading...'
|
||||
logs.value[newIndex].stdout = await get_output_by_filename(
|
||||
props.instance.path,
|
||||
logs.value[newIndex].log_type,
|
||||
logs.value[newIndex].filename,
|
||||
).catch(handleError)
|
||||
}
|
||||
})
|
||||
|
||||
if (logs.value.length > 1 && !props.playing) {
|
||||
selectedLogIndex.value = 1
|
||||
} else {
|
||||
async function deleteSelectedLog() {
|
||||
const log = selectedLog.value
|
||||
if (!log || log.live) return
|
||||
await delete_logs_by_filename(props.instance.path, log.log_type, log.filename)
|
||||
invalidate()
|
||||
const freshLogs = await getHistoricalLogs(props.instance.path)
|
||||
logs.value = buildLogList(freshLogs)
|
||||
selectedLogIndex.value = 0
|
||||
}
|
||||
|
||||
const deleteLog = async () => {
|
||||
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
|
||||
const deleteIndex = selectedLogIndex.value
|
||||
selectedLogIndex.value = deleteIndex - 1
|
||||
await delete_logs_by_filename(
|
||||
props.instance.path,
|
||||
logs.value[deleteIndex].log_type,
|
||||
logs.value[deleteIndex].filename,
|
||||
).catch(handleError)
|
||||
await setLogs()
|
||||
provideConsoleManager({
|
||||
logLines,
|
||||
logSources,
|
||||
activeLogSourceIndex: selectedLogIndex,
|
||||
showCommandInput: false,
|
||||
loading: ref(false),
|
||||
onClear: () => {
|
||||
if (!isLive.value) return
|
||||
void clearLive()
|
||||
},
|
||||
onDelete: deleteSelectedLog,
|
||||
deleteDisabled,
|
||||
deleteDisabledTooltip: 'Cannot delete latest.log while the instance is running',
|
||||
shareDisabled: computed(() => props.offline),
|
||||
emptyStateType: 'instance',
|
||||
crashAnalysis,
|
||||
onDismissCrash: () => {
|
||||
crashAnalysis.value = null
|
||||
},
|
||||
})
|
||||
|
||||
watch(selectedLogIndex, async (newIndex) => {
|
||||
if (newIndex === 0) return
|
||||
const log = filteredLogs.value[newIndex]
|
||||
if (!log) return
|
||||
|
||||
const cached = getHistoricalContent(log.filename)
|
||||
if (cached) {
|
||||
historicalConsole.clear()
|
||||
historicalConsole.addLegacyLog(cached)
|
||||
return
|
||||
}
|
||||
|
||||
const output = await get_output_by_filename(
|
||||
props.instance.path,
|
||||
log.log_type,
|
||||
log.filename,
|
||||
).catch(handleError)
|
||||
if (output) {
|
||||
historicalConsole.clear()
|
||||
historicalConsole.addLegacyLog(output)
|
||||
}
|
||||
})
|
||||
|
||||
selectedLogIndex.value = 0
|
||||
|
||||
if (!props.playing) {
|
||||
void analyseForCrash()
|
||||
}
|
||||
|
||||
const clearLiveLog = async () => {
|
||||
currentLiveLog.value = ''
|
||||
// does not reset cursor
|
||||
}
|
||||
const unlistenLog = await log_listener((payload) => {
|
||||
if (payload.profile_path_id !== profilePathId.value) return
|
||||
|
||||
const isLineLevel = (text, level) => {
|
||||
if ((text.includes('/INFO') || text.includes('[System] [CHAT]')) && level === 'info') {
|
||||
return true
|
||||
if (payload.type === 'log4j') {
|
||||
liveConsole.addLog4jEvent(payload)
|
||||
} else if (payload.type === 'legacy') {
|
||||
liveConsole.addLegacyLog(payload.message)
|
||||
}
|
||||
|
||||
if (text.includes('/WARN') && level === 'warn') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (text.includes('/DEBUG') && level === 'debug') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (text.includes('/TRACE') && level === 'trace') {
|
||||
return true
|
||||
}
|
||||
|
||||
const errorTriggers = ['/ERROR', 'Exception:', ':?]', 'Error', '[thread', ' at']
|
||||
if (level === 'error') {
|
||||
for (const trigger of errorTriggers) {
|
||||
if (text.includes(trigger)) return true
|
||||
}
|
||||
}
|
||||
|
||||
if (text.trim()[0] === '#' && level === 'comment') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const getLineWeight = (text) => {
|
||||
if (
|
||||
!logsColored ||
|
||||
isLineLevel(text, 'info') ||
|
||||
isLineLevel(text, 'debug') ||
|
||||
isLineLevel(text, 'trace')
|
||||
) {
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
if (isLineLevel(text, 'error') || isLineLevel(text, 'warn')) {
|
||||
return 'bold'
|
||||
}
|
||||
}
|
||||
|
||||
const getLineLevel = (text) => {
|
||||
for (const level of levels) {
|
||||
if (isLineLevel(text, level.toLowerCase())) {
|
||||
return level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getLineColor = (text, prefix) => {
|
||||
if (isLineLevel(text, 'comment')) {
|
||||
return 'var(--color-green)'
|
||||
}
|
||||
|
||||
if (!logsColored || text.includes('[System] [CHAT]')) {
|
||||
return 'var(--color-white)'
|
||||
}
|
||||
if (
|
||||
(isLineLevel(text, 'info') || isLineLevel(text, 'debug') || isLineLevel(text, 'trace')) &&
|
||||
prefix
|
||||
) {
|
||||
return 'var(--color-blue)'
|
||||
}
|
||||
if (isLineLevel(text, 'warn')) {
|
||||
return 'var(--color-orange)'
|
||||
}
|
||||
if (isLineLevel(text, 'error')) {
|
||||
return 'var(--color-red)'
|
||||
}
|
||||
}
|
||||
|
||||
const getLinePrefix = (text) => {
|
||||
if (text.includes(']:')) {
|
||||
return text.split(']:')[0] + ']:'
|
||||
}
|
||||
}
|
||||
|
||||
const getLineText = (text) => {
|
||||
if (text.includes(']:')) {
|
||||
if (text.split(']:').length > 2) {
|
||||
return text.split(']:').slice(1).join(']:')
|
||||
}
|
||||
return text.split(']:')[1]
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
function handleUserScroll() {
|
||||
if (!isAutoScrolling.value) {
|
||||
userScrolled.value = true
|
||||
}
|
||||
}
|
||||
|
||||
interval.value = setInterval(async () => {
|
||||
if (logs.value.length > 0) {
|
||||
logs.value[0] = await getLiveStdLog()
|
||||
const scroll = logContainer.value.getScroll()
|
||||
|
||||
// Allow resetting of userScrolled if the user scrolls to the bottom
|
||||
if (selectedLogIndex.value === 0) {
|
||||
if (scroll.end >= logContainer.value.$el.scrollHeight - 10) userScrolled.value = false
|
||||
if (!userScrolled.value) {
|
||||
await nextTick()
|
||||
isAutoScrolling.value = true
|
||||
logContainer.value.scrollToItem(displayProcessedLogs.value.length - 1)
|
||||
setTimeout(() => (isAutoScrolling.value = false), 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 250)
|
||||
})
|
||||
|
||||
const unlistenProcesses = await process_listener(async (e) => {
|
||||
if (e.profile_path_id !== profilePathId.value) return
|
||||
if (e.event === 'launched') {
|
||||
currentLiveLog.value = ''
|
||||
currentLiveLogCursor.value = 0
|
||||
liveConsole.clear()
|
||||
invalidate()
|
||||
selectedLogIndex.value = 0
|
||||
}
|
||||
if (e.event === 'finished') {
|
||||
currentLiveLog.value = ''
|
||||
currentLiveLogCursor.value = 0
|
||||
userScrolled.value = false
|
||||
await setLogs()
|
||||
selectedLogIndex.value = 1
|
||||
invalidate()
|
||||
const freshLogs = await getHistoricalLogs(props.instance.path)
|
||||
logs.value = buildLogList(freshLogs)
|
||||
void analyseForCrash()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
logContainer.value.$el.addEventListener('scroll', handleUserScroll)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
logContainer.value.$el.removeEventListener('scroll', handleUserScroll)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval.value)
|
||||
unlistenLog()
|
||||
unlistenProcesses()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.log-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: var(--mono-font);
|
||||
background-color: var(--color-accent-contrast);
|
||||
color: var(--color-contrast);
|
||||
border-radius: var(--radius-lg);
|
||||
padding-top: 1.5rem;
|
||||
overflow-x: auto; /* Enables horizontal scrolling */
|
||||
overflow-y: hidden; /* Disables vertical scrolling on this wrapper */
|
||||
white-space: nowrap; /* Keeps content on a single line */
|
||||
white-space: normal;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
margin-bottom: 0.3rem;
|
||||
font-size: 1rem;
|
||||
|
||||
svg {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
padding: 0.6rem;
|
||||
flex-direction: row;
|
||||
overflow: auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
&::-webkit-scrollbar-track,
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.vue-recycle-scroller__item-wrapper) {
|
||||
overflow: visible; /* Enables horizontal scrolling */
|
||||
}
|
||||
|
||||
:deep(.vue-recycle-scroller) {
|
||||
&::-webkit-scrollbar-corner {
|
||||
background-color: var(--color-bg);
|
||||
border-radius: 0 0 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.scroller {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user {
|
||||
height: 32%;
|
||||
padding: 0 1.5rem;
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
user-select: text;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,65 +1,67 @@
|
||||
<template>
|
||||
<ContentPageLayout>
|
||||
<template #modals>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
:share-title="formatMessage(messages.shareTitle)"
|
||||
:share-text="formatMessage(messages.shareText)"
|
||||
:open-in-new-tab="false"
|
||||
/>
|
||||
<ModpackContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="linkedModpackProject?.title"
|
||||
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
|
||||
:enable-toggle="!props.isServerInstance"
|
||||
:get-overflow-options="getOverflowOptions"
|
||||
:switch-version="handleSwitchVersion"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@bulk:enable="handleModpackContentBulkToggle"
|
||||
@bulk:disable="handleModpackContentBulkToggle"
|
||||
/>
|
||||
<ConfirmModpackUpdateModal
|
||||
ref="modpackUpdateConfirmModal"
|
||||
:downgrade="isModpackUpdateDowngrade"
|
||||
:backup-tip="
|
||||
[linkedModpackProject?.title, pendingModpackUpdateVersion?.version_number]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
"
|
||||
@confirm="handleModpackUpdateConfirm"
|
||||
@cancel="handleModpackUpdateCancel"
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ContentUpdaterModal
|
||||
v-if="updatingProject || updatingModpack"
|
||||
ref="contentUpdaterModal"
|
||||
:versions="updatingProjectVersions"
|
||||
:current-game-version="instance.game_version"
|
||||
:current-loader="instance.loader"
|
||||
:current-version-id="
|
||||
updatingModpack
|
||||
? (instance.linked_data?.version_id ?? '')
|
||||
: (updatingProject?.version?.id ?? '')
|
||||
"
|
||||
:is-app="true"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
||||
"
|
||||
:project-name="
|
||||
updatingModpack
|
||||
? (linkedModpackProject?.title ?? formatMessage(commonMessages.modpackLabel))
|
||||
: (updatingProject?.project?.title ?? updatingProject?.file_name)
|
||||
"
|
||||
:loading="loadingVersions"
|
||||
:loading-changelog="loadingChangelog"
|
||||
@update="handleModalUpdate"
|
||||
@cancel="resetUpdateState"
|
||||
@version-select="handleVersionSelect"
|
||||
@version-hover="handleVersionHover"
|
||||
/>
|
||||
</template>
|
||||
</ContentPageLayout>
|
||||
<ReadyTransition :pending="loading">
|
||||
<ContentPageLayout>
|
||||
<template #modals>
|
||||
<ShareModalWrapper
|
||||
ref="shareModal"
|
||||
:share-title="formatMessage(messages.shareTitle)"
|
||||
:share-text="formatMessage(messages.shareText)"
|
||||
:open-in-new-tab="false"
|
||||
/>
|
||||
<ModpackContentModal
|
||||
ref="modpackContentModal"
|
||||
:modpack-name="linkedModpackProject?.title"
|
||||
:modpack-icon-url="linkedModpackProject?.icon_url ?? undefined"
|
||||
:enable-toggle="!props.isServerInstance"
|
||||
:get-overflow-options="getOverflowOptions"
|
||||
:switch-version="handleSwitchVersion"
|
||||
@update:enabled="handleModpackContentToggle"
|
||||
@bulk:enable="handleModpackContentBulkToggle"
|
||||
@bulk:disable="handleModpackContentBulkToggle"
|
||||
/>
|
||||
<ConfirmModpackUpdateModal
|
||||
ref="modpackUpdateConfirmModal"
|
||||
:downgrade="isModpackUpdateDowngrade"
|
||||
:backup-tip="
|
||||
[linkedModpackProject?.title, pendingModpackUpdateVersion?.version_number]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
"
|
||||
@confirm="handleModpackUpdateConfirm"
|
||||
@cancel="handleModpackUpdateCancel"
|
||||
/>
|
||||
<ExportModal v-if="projects.length > 0" ref="exportModal" :instance="instance" />
|
||||
<ContentUpdaterModal
|
||||
v-if="updatingProject || updatingModpack"
|
||||
ref="contentUpdaterModal"
|
||||
:versions="updatingProjectVersions"
|
||||
:current-game-version="instance.game_version"
|
||||
:current-loader="instance.loader"
|
||||
:current-version-id="
|
||||
updatingModpack
|
||||
? (instance.linked_data?.version_id ?? '')
|
||||
: (updatingProject?.version?.id ?? '')
|
||||
"
|
||||
:is-app="true"
|
||||
:project-type="updatingModpack ? 'modpack' : updatingProject?.project_type"
|
||||
:project-icon-url="
|
||||
updatingModpack ? linkedModpackProject?.icon_url : updatingProject?.project?.icon_url
|
||||
"
|
||||
:project-name="
|
||||
updatingModpack
|
||||
? (linkedModpackProject?.title ?? formatMessage(commonMessages.modpackLabel))
|
||||
: (updatingProject?.project?.title ?? updatingProject?.file_name)
|
||||
"
|
||||
:loading="loadingVersions"
|
||||
:loading-changelog="loadingChangelog"
|
||||
@update="handleModalUpdate"
|
||||
@cancel="resetUpdateState"
|
||||
@version-select="handleVersionSelect"
|
||||
@version-hover="handleVersionHover"
|
||||
/>
|
||||
</template>
|
||||
</ContentPageLayout>
|
||||
</ReadyTransition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -82,6 +84,7 @@ import {
|
||||
type OverflowMenuOption,
|
||||
provideAppBackup,
|
||||
provideContentManager,
|
||||
ReadyTransition,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
@@ -340,7 +343,7 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
|
||||
}
|
||||
try {
|
||||
await remove_project(props.instance.path, mod.file_path!)
|
||||
const newPath = await add_project_from_version(props.instance.path, version.id)
|
||||
const newPath = await add_project_from_version(props.instance.path, version.id, 'standalone')
|
||||
|
||||
const profile = await get(props.instance.path).catch(handleError)
|
||||
if (profile) {
|
||||
|
||||
@@ -37,22 +37,109 @@
|
||||
:description="formatMessage(messages.deleteWorldDescription, { name: worldToDelete?.name })"
|
||||
@proceed="proceedDeleteWorld"
|
||||
/>
|
||||
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:spellcheck="false"
|
||||
input-class="!h-10"
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
clearable
|
||||
:placeholder="
|
||||
formatMessage(messages.searchWorldsPlaceholder, { count: dedupedWorlds.length })
|
||||
"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<ReadyTransition :pending="worldsReadyPending">
|
||||
<div v-if="dedupedWorlds.length > 0" class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
:icon="SearchIcon"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:spellcheck="false"
|
||||
input-class="!h-10"
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
clearable
|
||||
:placeholder="
|
||||
formatMessage(messages.searchWorldsPlaceholder, { count: dedupedWorlds.length })
|
||||
"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<FilterIcon class="size-5 text-secondary" />
|
||||
<button
|
||||
:class="filterPillClass(selectedFilters.length === 0)"
|
||||
@click="selectedFilters = []"
|
||||
>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.id"
|
||||
:class="filterPillClass(selectedFilters.includes(option.id))"
|
||||
@click="toggleFilter(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
<ButtonStyled type="transparent" hover-color-fill="none">
|
||||
<button :disabled="refreshingAll" @click="refreshAllWorlds">
|
||||
<RefreshCwIcon :class="refreshingAll ? 'animate-spin' : ''" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<WorldItem
|
||||
v-for="world in filteredWorlds"
|
||||
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
||||
:world="world"
|
||||
:managed="world.type === 'server' ? isManagedServerWorld(world) : false"
|
||||
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
||||
:supports-server-quick-play="supportsServerQuickPlay"
|
||||
:supports-world-quick-play="supportsWorldQuickPlay"
|
||||
:current-protocol="protocolVersion"
|
||||
:playing-instance="playing"
|
||||
:playing-world="worldsMatch(world, worldPlaying)"
|
||||
:starting-instance="startingInstance"
|
||||
:refreshing="world.type === 'server' ? serverData[world.address]?.refreshing : undefined"
|
||||
:server-status="world.type === 'server' ? serverData[world.address]?.status : undefined"
|
||||
:rendered-motd="
|
||||
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
|
||||
"
|
||||
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
|
||||
@play="() => joinWorld(world)"
|
||||
@stop="() => emit('stop')"
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
@edit="
|
||||
() =>
|
||||
world.type === 'singleplayer'
|
||||
? editWorldModal?.show(world)
|
||||
: isManagedServerWorld(world)
|
||||
? undefined
|
||||
: editServerModal?.show(world)
|
||||
"
|
||||
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else
|
||||
type="empty-inbox"
|
||||
:heading="formatMessage(messages.noWorldsHeading)"
|
||||
:description="formatMessage(messages.noWorldsDescription)"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
@@ -70,94 +157,9 @@
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<FilterIcon class="size-5 text-secondary" />
|
||||
<button
|
||||
:class="filterPillClass(selectedFilters.length === 0)"
|
||||
@click="selectedFilters = []"
|
||||
>
|
||||
{{ formatMessage(commonMessages.allProjectType) }}
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.id"
|
||||
:class="filterPillClass(selectedFilters.includes(option.id))"
|
||||
@click="toggleFilter(option.id)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
<ButtonStyled type="transparent" hover-color-fill="none">
|
||||
<button :disabled="refreshingAll" @click="refreshAllWorlds">
|
||||
<RefreshCwIcon :class="refreshingAll ? 'animate-spin' : ''" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<WorldItem
|
||||
v-for="world in filteredWorlds"
|
||||
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
||||
:world="world"
|
||||
:managed="world.type === 'server' ? isManagedServerWorld(world) : false"
|
||||
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
||||
:supports-server-quick-play="supportsServerQuickPlay"
|
||||
:supports-world-quick-play="supportsWorldQuickPlay"
|
||||
:current-protocol="protocolVersion"
|
||||
:playing-instance="playing"
|
||||
:playing-world="worldsMatch(world, worldPlaying)"
|
||||
:starting-instance="startingInstance"
|
||||
:refreshing="world.type === 'server' ? serverData[world.address]?.refreshing : undefined"
|
||||
:server-status="world.type === 'server' ? serverData[world.address]?.status : undefined"
|
||||
:rendered-motd="
|
||||
world.type === 'server' ? serverData[world.address]?.renderedMotd : undefined
|
||||
"
|
||||
:game-mode="world.type === 'singleplayer' ? GAME_MODES[world.game_mode] : undefined"
|
||||
@play="() => joinWorld(world)"
|
||||
@stop="() => emit('stop')"
|
||||
@refresh="() => refreshServer((world as ServerWorld).address)"
|
||||
@edit="
|
||||
() =>
|
||||
world.type === 'singleplayer'
|
||||
? editWorldModal?.show(world)
|
||||
: isManagedServerWorld(world)
|
||||
? undefined
|
||||
: editServerModal?.show(world)
|
||||
"
|
||||
@delete="() => !isManagedServerWorld(world) && promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else
|
||||
type="empty-inbox"
|
||||
:heading="formatMessage(messages.noWorldsHeading)"
|
||||
:description="formatMessage(messages.noWorldsDescription)"
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
class="!h-10 flex items-center gap-2"
|
||||
@click="
|
||||
router.push({ path: '/browse/server', query: { i: instance.path, from: 'worlds' } })
|
||||
"
|
||||
>
|
||||
<CompassIcon class="size-5" />
|
||||
<span>{{ formatMessage(messages.browseServers) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</template>
|
||||
</EmptyState>
|
||||
</ReadyTransition>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CompassIcon, FilterIcon, PlusIcon, RefreshCwIcon, SearchIcon } from '@modrinth/assets'
|
||||
@@ -169,11 +171,14 @@ import {
|
||||
GAME_MODES,
|
||||
type GameVersion,
|
||||
injectNotificationManager,
|
||||
ReadyTransition,
|
||||
StyledInput,
|
||||
useReadyState,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
@@ -344,11 +349,21 @@ function toggleFilter(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const refreshingAll = ref(false)
|
||||
const hadNoWorlds = ref(true)
|
||||
const startingInstance = ref(false)
|
||||
const worldPlaying = ref<World>()
|
||||
|
||||
const worldsQuery = useQuery({
|
||||
queryKey: computed(() => ['worlds', instance.value.path]),
|
||||
queryFn: () => refreshWorlds(instance.value.path),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const worldsReadyPending = useReadyState(worldsQuery)
|
||||
|
||||
const worlds = ref<World[]>([])
|
||||
const serverData = ref<Record<string, ServerData>>({})
|
||||
|
||||
@@ -357,8 +372,26 @@ const MAX_LINUX_REFRESHES = 3
|
||||
const isLinux = platform() === 'linux'
|
||||
const linuxRefreshCount = ref(0)
|
||||
|
||||
const protocolVersion = ref<ProtocolVersion | null>(
|
||||
await get_profile_protocol_version(instance.value.path),
|
||||
const protocolVersion = ref<ProtocolVersion | null>(null)
|
||||
|
||||
const gameVersions = ref<GameVersion[]>([])
|
||||
const supportsServerQuickPlay = computed(() =>
|
||||
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
const supportsWorldQuickPlay = computed(() =>
|
||||
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => worldsQuery.data.value,
|
||||
(data) => {
|
||||
if (data) {
|
||||
worlds.value = [...data]
|
||||
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
hadNoWorlds.value = worlds.value.length === 0
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
const managedServerName = ref<string | null>(null)
|
||||
const managedServerAddress = ref<string | null>(null)
|
||||
@@ -387,8 +420,8 @@ async function refreshManagedServerMetadata() {
|
||||
|
||||
try {
|
||||
const [project, projectV3] = await Promise.all([
|
||||
get_project(projectId, 'bypass'),
|
||||
get_project_v3(projectId, 'bypass'),
|
||||
get_project(projectId),
|
||||
get_project_v3(projectId),
|
||||
])
|
||||
|
||||
if (projectV3?.minecraft_server == null) {
|
||||
@@ -424,22 +457,40 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
let unlistenProfile: (() => void) | null = null
|
||||
let worldsTabAlive = true
|
||||
|
||||
console.info(`Handling profile event '${e.event}' for profile: ${e.profile_path_id}`)
|
||||
async function initWorldsTab() {
|
||||
const [_unlistenProfile, resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
|
||||
if (e.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
if (isLinux) linuxRefreshCount.value++
|
||||
console.info(`Handling profile event '${e.event}' for profile: ${e.profile_path_id}`)
|
||||
|
||||
await refreshAllWorlds()
|
||||
if (e.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
if (isLinux) linuxRefreshCount.value++
|
||||
|
||||
await refreshAllWorlds()
|
||||
}
|
||||
|
||||
await handleDefaultProfileUpdateEvent(worlds.value, instance.value.path, e)
|
||||
}),
|
||||
get_profile_protocol_version(instance.value.path).catch(() => null),
|
||||
get_game_versions().catch(() => [] as GameVersion[]),
|
||||
])
|
||||
|
||||
if (!worldsTabAlive) {
|
||||
_unlistenProfile()
|
||||
return
|
||||
}
|
||||
|
||||
await handleDefaultProfileUpdateEvent(worlds.value, instance.value.path, e)
|
||||
})
|
||||
unlistenProfile = _unlistenProfile
|
||||
protocolVersion.value = resolvedProtocolVersion
|
||||
gameVersions.value = resolvedGameVersions
|
||||
}
|
||||
|
||||
await refreshAllWorlds()
|
||||
await initWorldsTab()
|
||||
|
||||
async function refreshServer(address: string) {
|
||||
if (!serverData.value[address]) {
|
||||
@@ -455,26 +506,10 @@ async function refreshAllWorlds() {
|
||||
console.log(`Already refreshing, cancelling refresh.`)
|
||||
return
|
||||
}
|
||||
await refreshManagedServerMetadata()
|
||||
|
||||
refreshingAll.value = true
|
||||
|
||||
worlds.value = await refreshWorlds(instance.value.path).finally(
|
||||
() => (refreshingAll.value = false),
|
||||
)
|
||||
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
|
||||
const hasNoWorlds = worlds.value.length === 0
|
||||
|
||||
if (hadNoWorlds.value && hasNoWorlds) {
|
||||
setTimeout(() => {
|
||||
refreshingAll.value = false
|
||||
}, 1000)
|
||||
} else {
|
||||
refreshingAll.value = false
|
||||
}
|
||||
|
||||
hadNoWorlds.value = hasNoWorlds
|
||||
await queryClient.invalidateQueries({ queryKey: ['worlds', instance.value.path] })
|
||||
refreshingAll.value = false
|
||||
}
|
||||
|
||||
async function addServer(server: ServerWorld) {
|
||||
@@ -589,14 +624,6 @@ function worldsMatch(world: World, other: World | undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []))
|
||||
const supportsServerQuickPlay = computed(() =>
|
||||
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
const supportsWorldQuickPlay = computed(() =>
|
||||
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||
)
|
||||
|
||||
const dedupedWorlds = computed(() => {
|
||||
const visibleWorlds: World[] = []
|
||||
const serverIndexByDomain = new Map<string, number>()
|
||||
@@ -746,7 +773,8 @@ async function proceedDeleteWorld() {
|
||||
worldToDelete.value = undefined
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
onBeforeUnmount(() => {
|
||||
worldsTabAlive = false
|
||||
unlistenProfile?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -426,7 +426,7 @@ export function createContentInstall(opts: {
|
||||
}
|
||||
|
||||
try {
|
||||
await add_project_from_version(instance.id, version.id)
|
||||
await add_project_from_version(instance.id, version.id, 'standalone')
|
||||
await installVersionDependencies(
|
||||
profile,
|
||||
version,
|
||||
@@ -484,7 +484,7 @@ export function createContentInstall(opts: {
|
||||
)
|
||||
if (!id) return
|
||||
|
||||
await add_project_from_version(id, version.id)
|
||||
await add_project_from_version(id, version.id, 'standalone')
|
||||
await opts.router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||
|
||||
const instance = await get(id)
|
||||
@@ -585,7 +585,7 @@ export function createContentInstall(opts: {
|
||||
const installedProjectIds: string[] = [project.id]
|
||||
addInstallingItem(instancePath, project, version)
|
||||
try {
|
||||
await add_project_from_version(instance.path, version.id)
|
||||
await add_project_from_version(instance.path, version.id, 'standalone')
|
||||
await installVersionDependencies(
|
||||
instance,
|
||||
version,
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { AbstractWebNotificationManager } from '@modrinth/ui'
|
||||
import type { AbstractPopupNotificationManager, AbstractWebNotificationManager } from '@modrinth/ui'
|
||||
|
||||
import { setupCreationModal } from './setup/creation-modal'
|
||||
import { setupFilePickerProvider } from './setup/file-picker'
|
||||
import { setupInstanceImportProvider } from './setup/instance-import'
|
||||
import { setupTagsProvider } from './setup/tags'
|
||||
|
||||
export function setupProviders(notificationManager: AbstractWebNotificationManager) {
|
||||
export function setupProviders(
|
||||
notificationManager: AbstractWebNotificationManager,
|
||||
popupNotificationManager: AbstractPopupNotificationManager,
|
||||
) {
|
||||
setupTagsProvider(notificationManager)
|
||||
setupFilePickerProvider()
|
||||
setupInstanceImportProvider(notificationManager)
|
||||
|
||||
return {
|
||||
...setupCreationModal(notificationManager),
|
||||
...setupCreationModal(notificationManager, popupNotificationManager),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { type AuthProvider, provideAuth } from '@modrinth/ui'
|
||||
import { type Ref, ref, watchEffect } from 'vue'
|
||||
import { computed, type Ref, ref, watchEffect } from 'vue'
|
||||
|
||||
type AppCredentials = {
|
||||
session?: string | null
|
||||
@@ -13,10 +13,12 @@ export function setupAuthProvider(
|
||||
) {
|
||||
const sessionToken = ref<string | null>(null)
|
||||
const user = ref<Labrinth.Users.v2.User | null>(null)
|
||||
const isReady = computed(() => credentials.value !== undefined)
|
||||
|
||||
const authProvider: AuthProvider = {
|
||||
session_token: sessionToken,
|
||||
user,
|
||||
isReady,
|
||||
requestSignIn,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
import type {
|
||||
AbstractPopupNotificationManager,
|
||||
AbstractWebNotificationManager,
|
||||
CreationFlowContextValue,
|
||||
CreationFlowModal,
|
||||
} from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { provide, ref, useTemplateRef } from 'vue'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
|
||||
import type ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_versions, get_search_results } from '@/helpers/cache.js'
|
||||
import { import_instance } from '@/helpers/import.js'
|
||||
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata.js'
|
||||
import { create_profile_and_install, create_profile_and_install_from_file } from '@/helpers/pack'
|
||||
import { create, list } from '@/helpers/profile.js'
|
||||
import type { InstanceLoader } from '@/helpers/types'
|
||||
|
||||
export function setupCreationModal(notificationManager: AbstractWebNotificationManager) {
|
||||
export function setupCreationModal(
|
||||
notificationManager: AbstractWebNotificationManager,
|
||||
popupNotificationManager: AbstractPopupNotificationManager,
|
||||
) {
|
||||
const { handleError } = notificationManager
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const messages = defineMessages({
|
||||
installingModpackTitle: {
|
||||
id: 'app.creation-modal.installing-modpack.title',
|
||||
defaultMessage: 'Installing modpack...',
|
||||
},
|
||||
installingModpackDescription: {
|
||||
id: 'app.creation-modal.installing-modpack.description',
|
||||
defaultMessage: '{fileName}',
|
||||
},
|
||||
})
|
||||
|
||||
const installationModal =
|
||||
useTemplateRef<ComponentExposed<typeof CreationFlowModal>>('installationModal')
|
||||
const unknownPackWarningModal =
|
||||
useTemplateRef<InstanceType<typeof UnknownPackWarningModal>>('unknownPackWarningModal')
|
||||
const modpackAlreadyInstalledModal = ref<InstanceType<typeof ModpackAlreadyInstalledModal>>()
|
||||
|
||||
function setModpackAlreadyInstalledModal(
|
||||
@@ -87,7 +108,24 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
||||
}
|
||||
|
||||
if (config.modpackFilePath.value) {
|
||||
await create_profile_and_install_from_file(config.modpackFilePath.value).catch(handleError)
|
||||
const waitingNotification = popupNotificationManager.addPopupNotification({
|
||||
title: formatMessage(messages.installingModpackTitle),
|
||||
text: formatMessage(messages.installingModpackDescription, {
|
||||
fileName: config.modpackFilePath.value.split('/').pop() ?? config.modpackFilePath.value,
|
||||
}),
|
||||
type: 'info',
|
||||
autoCloseMs: null,
|
||||
waiting: true,
|
||||
})
|
||||
|
||||
await create_profile_and_install_from_file(
|
||||
config.modpackFilePath.value,
|
||||
(createProfile, fileName) => {
|
||||
popupNotificationManager.removeNotification(waitingNotification.id)
|
||||
unknownPackWarningModal.value?.show(createProfile, fileName)
|
||||
},
|
||||
).catch(handleError)
|
||||
popupNotificationManager.removeNotification(waitingNotification.id)
|
||||
trackEvent('InstanceCreate', { source: 'CreationModalModpackFile' })
|
||||
return
|
||||
}
|
||||
@@ -160,11 +198,13 @@ export function setupCreationModal(notificationManager: AbstractWebNotificationM
|
||||
|
||||
return {
|
||||
installationModal,
|
||||
unknownPackWarningModal,
|
||||
fetchExistingInstanceNames,
|
||||
handleCreate,
|
||||
handleBrowseModpacks,
|
||||
searchModpacks,
|
||||
getProjectVersions,
|
||||
getLoaderManifest,
|
||||
setModpackAlreadyInstalledModal,
|
||||
handleModpackDuplicateCreateAnyway,
|
||||
handleModpackDuplicateGoToInstance,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { LoadingStateProvider } from '@modrinth/ui'
|
||||
import { createLoadingStateCore, provideLoadingState } from '@modrinth/ui'
|
||||
|
||||
/**
|
||||
* Source of truth for the desktop app's loading state.
|
||||
*
|
||||
* Owns the token-based ref-counter directly (no Pinia store). Consumers
|
||||
* obtain the same reactive state via `injectLoadingState()` from `@modrinth/ui`.
|
||||
*
|
||||
* Returns the provider so the call site (App.vue) can also use it directly
|
||||
* without a second injection round-trip.
|
||||
*/
|
||||
export function setupLoadingStateProvider(): LoadingStateProvider {
|
||||
const provider = createLoadingStateCore({ barEnabled: false })
|
||||
provideLoadingState(provider)
|
||||
return provider
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
createContext,
|
||||
type CreationFlowContextValue,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
type ServerFlowFrom = 'onboarding' | 'reset-server'
|
||||
type ServerInstallableType = 'modpack' | 'mod' | 'plugin' | 'datapack'
|
||||
|
||||
type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & {
|
||||
installing?: boolean
|
||||
installed?: boolean
|
||||
}
|
||||
|
||||
interface ServerModpackSelectionRequest {
|
||||
projectId: string
|
||||
versionId: string
|
||||
name: string
|
||||
iconUrl?: string
|
||||
}
|
||||
|
||||
interface ServerSetupModalHandle {
|
||||
show: () => void | Promise<void>
|
||||
hide: () => void
|
||||
ctx?: CreationFlowContextValue | null
|
||||
}
|
||||
|
||||
export interface ServerInstallContentContext {
|
||||
serverIdQuery: ComputedRef<string | null>
|
||||
worldIdQuery: ComputedRef<string | null>
|
||||
browseFrom: ComputedRef<string | null>
|
||||
serverFlowFrom: ComputedRef<ServerFlowFrom | null>
|
||||
isFromWorlds: ComputedRef<boolean>
|
||||
isServerContext: ComputedRef<boolean>
|
||||
isSetupServerContext: ComputedRef<boolean>
|
||||
effectiveServerWorldId: ComputedRef<string | null>
|
||||
serverContextServerData: Ref<Archon.Servers.v0.Server | null>
|
||||
serverContentProjectIds: Ref<Set<string>>
|
||||
serverBackUrl: ComputedRef<string>
|
||||
serverBackLabel: ComputedRef<string>
|
||||
serverBrowseHeading: ComputedRef<string>
|
||||
initServerContext: () => Promise<void>
|
||||
watchServerContextChanges: () => void
|
||||
searchServerModpacks: (
|
||||
query: string,
|
||||
limit?: number,
|
||||
) => Promise<Labrinth.Projects.v2.SearchResult>
|
||||
getServerProjectVersions: (projectId: string) => Promise<{ id: string }[]>
|
||||
enforceSetupModpackRoute: (currentProjectType: string | undefined) => void
|
||||
installProjectToServer: (project: InstallableSearchResult) => Promise<boolean>
|
||||
onServerFlowBack: () => void
|
||||
handleServerModpackFlowCreate: (config: CreationFlowContextValue) => Promise<void>
|
||||
markServerProjectInstalled: (id: string) => void
|
||||
}
|
||||
|
||||
export const [injectServerInstallContent, provideServerInstallContent] =
|
||||
createContext<ServerInstallContentContext>('Browse', 'serverInstallContent')
|
||||
|
||||
function readQueryString(value: unknown): string | null {
|
||||
if (Array.isArray(value)) return value[0] ?? null
|
||||
return typeof value === 'string' && value.length > 0 ? value : null
|
||||
}
|
||||
|
||||
export function createServerInstallContent(opts: {
|
||||
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
|
||||
}) {
|
||||
const { serverSetupModalRef } = opts
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const client = injectModrinthClient()
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const serverIdQuery = computed(() => readQueryString(route.query.sid))
|
||||
const worldIdQuery = computed(() => readQueryString(route.query.wid))
|
||||
const browseFrom = computed(() => readQueryString(route.query.from))
|
||||
const serverFlowFrom = computed<ServerFlowFrom | null>(() =>
|
||||
browseFrom.value === 'onboarding' || browseFrom.value === 'reset-server'
|
||||
? browseFrom.value
|
||||
: null,
|
||||
)
|
||||
|
||||
const isFromWorlds = computed(() => browseFrom.value === 'worlds')
|
||||
const isServerContext = computed(() => !!serverIdQuery.value)
|
||||
const isSetupServerContext = computed(() => !!serverIdQuery.value && !!serverFlowFrom.value)
|
||||
|
||||
const serverContextWorldId = ref<string | null>(worldIdQuery.value)
|
||||
const serverContextServerData = ref<Archon.Servers.v0.Server | null>(null)
|
||||
const serverContentProjectIds = ref<Set<string>>(new Set())
|
||||
const effectiveServerWorldId = computed(() => worldIdQuery.value ?? serverContextWorldId.value)
|
||||
|
||||
const serverBackUrl = computed(() => {
|
||||
const sid = serverIdQuery.value
|
||||
if (!sid) return '/hosting/manage'
|
||||
if (serverFlowFrom.value === 'onboarding') {
|
||||
return `/hosting/manage/${sid}?resumeModal=setup-type`
|
||||
}
|
||||
if (serverFlowFrom.value === 'reset-server') {
|
||||
return `/hosting/manage/${sid}?openSettings=installation`
|
||||
}
|
||||
return `/hosting/manage/${sid}/content`
|
||||
})
|
||||
const serverBackLabel = computed(() => {
|
||||
if (serverFlowFrom.value === 'onboarding') return 'Back to setup'
|
||||
if (serverFlowFrom.value === 'reset-server') return 'Cancel reset'
|
||||
return 'Back to server'
|
||||
})
|
||||
const serverBrowseHeading = computed(() => {
|
||||
if (serverFlowFrom.value === 'reset-server') {
|
||||
return 'Select modpack to install after reset'
|
||||
}
|
||||
return 'Install content to server'
|
||||
})
|
||||
|
||||
async function resolveServerContextWorldId(serverId: string) {
|
||||
try {
|
||||
const server = await client.archon.servers_v1.get(serverId)
|
||||
const activeWorld = server.worlds.find((world) => world.is_active)
|
||||
return activeWorld?.id ?? server.worlds[0]?.id ?? null
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshServerInstalledContent(serverId: string, worldId: string) {
|
||||
try {
|
||||
const content = await client.archon.content_v1.getAddons(serverId, worldId)
|
||||
const ids = new Set(
|
||||
(content.addons ?? [])
|
||||
.map((addon) => addon.project_id)
|
||||
.filter((projectId): projectId is string => !!projectId),
|
||||
)
|
||||
serverContentProjectIds.value = ids
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
}
|
||||
|
||||
async function initServerContext() {
|
||||
const sid = serverIdQuery.value
|
||||
if (!sid) return
|
||||
|
||||
try {
|
||||
serverContextServerData.value = await client.archon.servers_v0.get(sid)
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
|
||||
let resolvedWorldId = effectiveServerWorldId.value
|
||||
if (!resolvedWorldId) {
|
||||
resolvedWorldId = await resolveServerContextWorldId(sid)
|
||||
if (resolvedWorldId) {
|
||||
serverContextWorldId.value = resolvedWorldId
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedWorldId) {
|
||||
await refreshServerInstalledContent(sid, resolvedWorldId)
|
||||
}
|
||||
}
|
||||
|
||||
function watchServerContextChanges() {
|
||||
watch([serverIdQuery, effectiveServerWorldId], async ([sid, wid], [prevSid, prevWid]) => {
|
||||
if (!sid) {
|
||||
serverContextServerData.value = null
|
||||
serverContentProjectIds.value = new Set()
|
||||
return
|
||||
}
|
||||
|
||||
if (sid !== prevSid) {
|
||||
serverContentProjectIds.value = new Set()
|
||||
try {
|
||||
serverContextServerData.value = await client.archon.servers_v0.get(sid)
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
}
|
||||
|
||||
if (wid && (sid !== prevSid || wid !== prevWid)) {
|
||||
await refreshServerInstalledContent(sid, wid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeLoader(loader: string) {
|
||||
return loader.toLowerCase().replaceAll('_', '').replaceAll('-', '').replaceAll(' ', '')
|
||||
}
|
||||
|
||||
function getCompatibleLoaders(loader: string) {
|
||||
const normalized = normalizeLoader(loader)
|
||||
if (!normalized) return new Set<string>()
|
||||
if (normalized === 'paper' || normalized === 'purpur' || normalized === 'spigot') {
|
||||
return new Set(['paper', 'purpur', 'spigot', 'bukkit'])
|
||||
}
|
||||
if (normalized === 'neoforge' || normalized === 'neo') {
|
||||
return new Set(['neoforge', 'neo'])
|
||||
}
|
||||
return new Set([normalized])
|
||||
}
|
||||
|
||||
function enforceSetupModpackRoute(currentProjectType: string | undefined) {
|
||||
if (!isSetupServerContext.value || currentProjectType === 'modpack') return
|
||||
router.replace({
|
||||
path: '/browse/modpack',
|
||||
query: route.query,
|
||||
})
|
||||
}
|
||||
|
||||
async function searchServerModpacks(query: string, limit: number = 10) {
|
||||
return client.labrinth.projects_v2.search({
|
||||
query: query || undefined,
|
||||
new_filters:
|
||||
'project_types = "modpack" AND (client_side = "optional" OR client_side = "required") AND server_side = "required"',
|
||||
limit,
|
||||
})
|
||||
}
|
||||
|
||||
async function getServerProjectVersions(projectId: string) {
|
||||
const versions = await client.labrinth.versions_v3.getProjectVersions(projectId)
|
||||
return versions.map((version) => ({ id: version.id }))
|
||||
}
|
||||
|
||||
async function openServerModpackInstallFlow(request: ServerModpackSelectionRequest) {
|
||||
if (!serverIdQuery.value || !effectiveServerWorldId.value) {
|
||||
throw new Error('Missing server context')
|
||||
}
|
||||
|
||||
const modalInstance = serverSetupModalRef.value
|
||||
if (!modalInstance) return
|
||||
|
||||
modalInstance.show()
|
||||
await nextTick()
|
||||
|
||||
const ctx = modalInstance.ctx
|
||||
if (!ctx) return
|
||||
|
||||
ctx.setupType.value = 'modpack'
|
||||
ctx.modpackSelection.value = {
|
||||
projectId: request.projectId,
|
||||
versionId: request.versionId,
|
||||
name: request.name,
|
||||
iconUrl: request.iconUrl,
|
||||
}
|
||||
ctx.modal.value?.setStage('final-config')
|
||||
}
|
||||
|
||||
function getCurrentServerInstallType(): ServerInstallableType {
|
||||
const raw = Array.isArray(route.params.projectType)
|
||||
? route.params.projectType[0]
|
||||
: route.params.projectType
|
||||
if (raw === 'modpack' || raw === 'mod' || raw === 'plugin' || raw === 'datapack') {
|
||||
return raw
|
||||
}
|
||||
throw new Error('This content type cannot be installed to a server from browse.')
|
||||
}
|
||||
|
||||
async function installProjectToServer(project: InstallableSearchResult) {
|
||||
const contentType = getCurrentServerInstallType()
|
||||
const sid = serverIdQuery.value
|
||||
const wid = effectiveServerWorldId.value
|
||||
if (!sid || !wid) {
|
||||
throw new Error('No server world is available for install.')
|
||||
}
|
||||
|
||||
if (contentType === 'modpack') {
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
|
||||
include_changelog: false,
|
||||
})
|
||||
const versionId = versions[0]?.id ?? project.version_id
|
||||
if (!versionId) {
|
||||
throw new Error('No version found for this modpack')
|
||||
}
|
||||
|
||||
await openServerModpackInstallFlow({
|
||||
projectId: project.project_id,
|
||||
versionId,
|
||||
name: project.name,
|
||||
iconUrl: project.icon_url ?? undefined,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
|
||||
include_changelog: false,
|
||||
})
|
||||
const serverLoader = (serverContextServerData.value?.loader ?? '').toLowerCase()
|
||||
const serverGameVersion = (serverContextServerData.value?.mc_version ?? '').trim()
|
||||
const compatibleLoaders = getCompatibleLoaders(serverLoader)
|
||||
|
||||
const hasGameVersionMatch = (version: Labrinth.Versions.v2.Version) =>
|
||||
!serverGameVersion || version.game_versions.includes(serverGameVersion)
|
||||
const hasLoaderMatch = (version: Labrinth.Versions.v2.Version) => {
|
||||
if (contentType === 'datapack') return true
|
||||
if (compatibleLoaders.size === 0) return true
|
||||
return version.loaders.some((loader) => compatibleLoaders.has(normalizeLoader(loader)))
|
||||
}
|
||||
|
||||
let matchingVersion = versions.find(
|
||||
(version) => hasGameVersionMatch(version) && hasLoaderMatch(version),
|
||||
)
|
||||
if (!matchingVersion) {
|
||||
matchingVersion = versions.find((version) => hasLoaderMatch(version))
|
||||
}
|
||||
if (!matchingVersion) {
|
||||
matchingVersion = versions.find((version) => hasGameVersionMatch(version))
|
||||
}
|
||||
if (!matchingVersion) {
|
||||
matchingVersion = versions[0]
|
||||
}
|
||||
if (!matchingVersion) {
|
||||
throw new Error('No installable version was found for this project.')
|
||||
}
|
||||
|
||||
await client.archon.content_v1.addAddon(sid, wid, {
|
||||
project_id: matchingVersion.project_id,
|
||||
version_id: matchingVersion.id,
|
||||
})
|
||||
|
||||
serverContentProjectIds.value = new Set([...serverContentProjectIds.value, project.project_id])
|
||||
return true
|
||||
}
|
||||
|
||||
function onServerFlowBack() {
|
||||
serverSetupModalRef.value?.hide()
|
||||
}
|
||||
|
||||
async function handleServerModpackFlowCreate(config: CreationFlowContextValue) {
|
||||
const sid = serverIdQuery.value
|
||||
const wid = effectiveServerWorldId.value
|
||||
if (!sid || !wid || !config.modpackSelection.value) {
|
||||
config.loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await client.archon.content_v1.installContent(sid, wid, {
|
||||
content_variant: 'modpack',
|
||||
spec: {
|
||||
platform: 'modrinth',
|
||||
project_id: config.modpackSelection.value.projectId,
|
||||
version_id: config.modpackSelection.value.versionId,
|
||||
},
|
||||
soft_override: false,
|
||||
properties: config.buildProperties(),
|
||||
} satisfies Archon.Content.v1.InstallWorldContent)
|
||||
serverSetupModalRef.value?.hide()
|
||||
|
||||
if (serverFlowFrom.value === 'onboarding') {
|
||||
await client.archon.servers_v1.endIntro(sid)
|
||||
await router.push(`/hosting/manage/${sid}/content`)
|
||||
return
|
||||
}
|
||||
|
||||
await router.push(`/hosting/manage/${sid}?openSettings=installation`)
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
config.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function markServerProjectInstalled(id: string) {
|
||||
serverContentProjectIds.value = new Set([...serverContentProjectIds.value, id])
|
||||
}
|
||||
|
||||
return {
|
||||
serverIdQuery,
|
||||
worldIdQuery,
|
||||
browseFrom,
|
||||
serverFlowFrom,
|
||||
isFromWorlds,
|
||||
isServerContext,
|
||||
isSetupServerContext,
|
||||
effectiveServerWorldId,
|
||||
serverContextServerData,
|
||||
serverContentProjectIds,
|
||||
serverBackUrl,
|
||||
serverBackLabel,
|
||||
serverBrowseHeading,
|
||||
initServerContext,
|
||||
watchServerContextChanges,
|
||||
searchServerModpacks,
|
||||
getServerProjectVersions,
|
||||
enforceSetupModpackRoute,
|
||||
installProjectToServer,
|
||||
onServerFlowBack,
|
||||
handleServerModpackFlowCreate,
|
||||
markServerProjectInstalled,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ServersManagePageIndex } from '@modrinth/ui'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import * as Pages from '@/pages'
|
||||
import * as Hosting from '@/pages/hosting/manage'
|
||||
import * as Instance from '@/pages/instance'
|
||||
import * as Library from '@/pages/library'
|
||||
import * as Project from '@/pages/project'
|
||||
@@ -31,11 +31,50 @@ export default new createRouter({
|
||||
{
|
||||
path: '/hosting/manage/',
|
||||
name: 'Servers',
|
||||
component: ServersManagePageIndex,
|
||||
component: Pages.Servers,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Servers' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/hosting/manage/:id',
|
||||
name: 'ServerManage',
|
||||
component: Hosting.Index,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'ServerManageOverview',
|
||||
component: Hosting.Overview,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'content',
|
||||
name: 'ServerManageContent',
|
||||
component: Hosting.Content,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'ServerManageFiles',
|
||||
component: Hosting.Files,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'backups',
|
||||
name: 'ServerManageBackups',
|
||||
component: Hosting.Backups,
|
||||
meta: {
|
||||
breadcrumb: [{ name: '?Server' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/browse/:projectType',
|
||||
name: 'Discover content',
|
||||
@@ -88,6 +127,13 @@ export default new createRouter({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:projectType(mod|plugin|datapack|resourcepack|shader|modpack)/:id/:rest(.*)*',
|
||||
redirect: (to) => {
|
||||
const rest = to.params.rest ? `/${[].concat(to.params.rest).join('/')}` : ''
|
||||
return `/project/${to.params.id}${rest}${to.hash}`
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/project/:id',
|
||||
name: 'Project',
|
||||
@@ -194,6 +240,7 @@ export default new createRouter({
|
||||
component: Instance.Logs,
|
||||
meta: {
|
||||
useRootContext: true,
|
||||
// renderMode: 'fixed',
|
||||
breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Logs' }],
|
||||
},
|
||||
},
|
||||
@@ -202,7 +249,8 @@ export default new createRouter({
|
||||
],
|
||||
linkActiveClass: 'router-link-active',
|
||||
linkExactActiveClass: 'router-link-exact-active',
|
||||
scrollBehavior() {
|
||||
scrollBehavior(to, from) {
|
||||
if (to.path === from.path) return
|
||||
// Sometimes Vue's scroll behavior is not working as expected, so we need to manually scroll to top (especially on Linux)
|
||||
document.querySelector('.app-viewport')?.scrollTo(0, 0)
|
||||
return {
|
||||
|
||||
@@ -177,7 +177,7 @@ export const installVersionDependencies = async (profile, version, onDepInstalli
|
||||
const batch = queuedInstalls.slice(i, i + batchSize)
|
||||
await Promise.all(
|
||||
batch.map(async ({ versionId }) => {
|
||||
await add_project_from_version(profile.path, versionId)
|
||||
await add_project_from_version(profile.path, versionId, 'dependency')
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useLoading = defineStore('loadingStore', {
|
||||
state: () => ({
|
||||
loading: false,
|
||||
barEnabled: false,
|
||||
}),
|
||||
actions: {
|
||||
setEnabled(enabled) {
|
||||
this.barEnabled = enabled
|
||||
},
|
||||
startLoading() {
|
||||
this.loading = true
|
||||
},
|
||||
stopLoading() {
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user