mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
305
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73ed1bc249 | ||
|
|
3258d7dbdf | ||
|
|
b5d1aeda85 | ||
|
|
1cedbe5fda | ||
|
|
2c9bf58d1f | ||
|
|
a92b5b08df | ||
|
|
01d3fb47c4 | ||
|
|
9404d46782 | ||
|
|
926c72de42 | ||
|
|
9729737d7d | ||
|
|
97a8c11b50 | ||
|
|
4d654162eb | ||
|
|
33b1419bdf | ||
|
|
c28ba2e6a4 | ||
|
|
c7ba6ba8b2 | ||
|
|
7366c32df3 | ||
|
|
707e219ff8 | ||
|
|
dfe12d4ecb | ||
|
|
c653228fe7 | ||
|
|
352a196795 | ||
|
|
cef9b1efe5 | ||
|
|
7d6c54cff9 | ||
|
|
bd97ace974 | ||
|
|
58ad58f958 | ||
|
|
d907083d83 | ||
|
|
8371ff641a | ||
|
|
b1cd16f966 | ||
|
|
40a06921ea | ||
|
|
a7dc063e08 | ||
|
|
64b61d8fd0 | ||
|
|
5e7d4cc838 | ||
|
|
a0c80b13a4 | ||
|
|
3c051f5b1d | ||
|
|
940a796ba5 | ||
|
|
6ee5e4df19 | ||
|
|
d61397097c | ||
|
|
cfe45b368c | ||
|
|
6b0a0c1897 | ||
|
|
f27691340a | ||
|
|
c3a58aba9e | ||
|
|
1550dfb3f0 | ||
|
|
4762a0a725 | ||
|
|
cf82943766 | ||
|
|
b527e8f8f2 | ||
|
|
3c53b5793b | ||
|
|
e6afc6f4f3 | ||
|
|
05699a90d6 | ||
|
|
142d560f76 | ||
|
|
e80e27884e | ||
|
|
75788938b5 | ||
|
|
2570cf1bd7 | ||
|
|
b0cca873b6 | ||
|
|
325926ad9b | ||
|
|
34b87991bc | ||
|
|
cc8d556448 | ||
|
|
71286f7b2b | ||
|
|
c29973ec1a | ||
|
|
8c95f0bb81 | ||
|
|
627ab6734b | ||
|
|
36e1cbbf3e | ||
|
|
93fe87e57f | ||
|
|
02363c27a2 | ||
|
|
67e1743d6c | ||
|
|
cef20abceb | ||
|
|
62cedab6dd | ||
|
|
ed5a74a9d3 | ||
|
|
e2318ee776 | ||
|
|
a29c1c61c4 | ||
|
|
206813b74c | ||
|
|
99611d22c7 | ||
|
|
11b2b6e6c0 | ||
|
|
f49951084e | ||
|
|
047b8c3bf7 | ||
|
|
5c1ffd9ff2 | ||
|
|
670b3c17b6 | ||
|
|
ec7c538888 | ||
|
|
b88341e3a8 | ||
|
|
2048d8008a | ||
|
|
527f2f800b | ||
|
|
c7d3229fd3 | ||
|
|
ec49a3b051 | ||
|
|
cf1d948030 | ||
|
|
fe8fa4b6f7 | ||
|
|
2c62cf1d12 | ||
|
|
84b91f32f8 | ||
|
|
64edf2ddeb | ||
|
|
2f248027d6 | ||
|
|
f96520638b | ||
|
|
9e5d29ced6 | ||
|
|
3e15d0b287 | ||
|
|
bcce7e28fd | ||
|
|
3889d0f5ec | ||
|
|
6f44c5b039 | ||
|
|
ea967845d9 | ||
|
|
a6967cf9cb | ||
|
|
5bf92863b0 | ||
|
|
71b6ecc10c | ||
|
|
ed28bc7551 | ||
|
|
3e4197db7c | ||
|
|
e12230ff59 | ||
|
|
aeb9f5a075 | ||
|
|
8b17441f40 | ||
|
|
f9d47e8edc | ||
|
|
a58bc3dc21 | ||
|
|
1e46444fb0 | ||
|
|
1511e55597 | ||
|
|
5727e156ed | ||
|
|
657186398d | ||
|
|
3ab2273782 | ||
|
|
893ec00fc6 | ||
|
|
aa7dd1d210 | ||
|
|
f8733b0488 | ||
|
|
d077d44540 | ||
|
|
4e1a61d8b6 | ||
|
|
71dee4de40 | ||
|
|
f74fad0cae | ||
|
|
07e81ac036 | ||
|
|
6e7835fb35 | ||
|
|
2f95c4c441 | ||
|
|
451b2d0e44 | ||
|
|
215643c846 | ||
|
|
d8b1415f9c | ||
|
|
3eeb549d20 | ||
|
|
c3fe7b4232 | ||
|
|
079a10bba9 | ||
|
|
3c3d5702ba | ||
|
|
a34576a2c3 | ||
|
|
d8e4915a31 | ||
|
|
ed723fa186 | ||
|
|
d6c8d4475b | ||
|
|
302b60d89c | ||
|
|
f106dc580f | ||
|
|
244c263e40 | ||
|
|
48bb44155d | ||
|
|
3f2e76ae7e | ||
|
|
6479eca0e9 | ||
|
|
8767bc9184 | ||
|
|
d1185414b6 | ||
|
|
510439acbf | ||
|
|
c564495e11 | ||
|
|
8dd1490c8a | ||
|
|
b72bc18a6b | ||
|
|
cee942dcef | ||
|
|
02a7774722 | ||
|
|
e9eb98f97e | ||
|
|
e5bbd9d409 | ||
|
|
83dddfd512 | ||
|
|
0ffdabb2a3 | ||
|
|
d87f93fdd5 | ||
|
|
f6fa486dea | ||
|
|
78d978b22e | ||
|
|
71559d62c8 | ||
|
|
538eda6976 | ||
|
|
882b01c7c9 | ||
|
|
a192f7857e | ||
|
|
3bf0f91cf0 | ||
|
|
3083dcd932 | ||
|
|
c8c79a6c74 | ||
|
|
f5462b6dd8 | ||
|
|
1ddbae40b7 | ||
|
|
a4f3c63fcc | ||
|
|
02e10be4db | ||
|
|
e0056bfc40 | ||
|
|
612934bf34 | ||
|
|
86d377b915 | ||
|
|
ad99ac039b | ||
|
|
6d3fdb680c | ||
|
|
840b556c51 | ||
|
|
12e5f02e57 | ||
|
|
ca1b36efde | ||
|
|
a5417e0851 | ||
|
|
45398c546c | ||
|
|
7e769c720b | ||
|
|
c1c86e3b72 | ||
|
|
c7602602e5 | ||
|
|
07f9e3aedc | ||
|
|
a79b8e0777 | ||
|
|
671f6d264a | ||
|
|
e231df1f97 | ||
|
|
3052a14d95 | ||
|
|
e8665f43ca | ||
|
|
cba4550be4 | ||
|
|
384556a810 | ||
|
|
a082e8597c | ||
|
|
7048a35e9f | ||
|
|
c166ce52b3 | ||
|
|
9c99518497 | ||
|
|
758ed818c8 | ||
|
|
83e45d7a5c | ||
|
|
77b30b27fe | ||
|
|
3d7aea5a45 | ||
|
|
fd5d2797b3 | ||
|
|
ec85d9de1c | ||
|
|
22415a4cc6 | ||
|
|
871672d8bf | ||
|
|
e8dc3c3150 | ||
|
|
56dae8f104 | ||
|
|
ae9ca4db18 | ||
|
|
4eeb53c429 | ||
|
|
c69f24f94d | ||
|
|
de07bcff7d | ||
|
|
beff44767e | ||
|
|
5875e4332f | ||
|
|
f9c078d29d | ||
|
|
99ac6b87b1 | ||
|
|
159c6205ef | ||
|
|
bb4862daa6 | ||
|
|
6ed56a4756 | ||
|
|
93b21bb107 | ||
|
|
b442fa4cca | ||
|
|
118046d690 | ||
|
|
b6bca2aaeb | ||
|
|
dcab665455 | ||
|
|
2f311643a0 | ||
|
|
e13a89dd72 | ||
|
|
565ac2cb53 | ||
|
|
7d6f77bebf | ||
|
|
b53887997c | ||
|
|
9a499af03e | ||
|
|
f6edc3ab58 | ||
|
|
c1d7aa494c | ||
|
|
a4c8154438 | ||
|
|
7dbbbe590f | ||
|
|
8a72ee9968 | ||
|
|
2da2035a6f | ||
|
|
4c59a5e51d | ||
|
|
678f8049e3 | ||
|
|
eb9c3477ff | ||
|
|
f857d19aee | ||
|
|
5b59e39a8a | ||
|
|
9015ff0971 | ||
|
|
1fd58e0a5a | ||
|
|
4348664618 | ||
|
|
596fd81348 | ||
|
|
388ba61d15 | ||
|
|
be618d96f4 | ||
|
|
c2359275ff | ||
|
|
edbb3fbd55 | ||
|
|
9403462915 | ||
|
|
264aade726 | ||
|
|
4ddb5640cf | ||
|
|
1875b89556 | ||
|
|
38a39feef1 | ||
|
|
7e149e1cf1 | ||
|
|
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 |
@@ -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.
|
||||
@@ -9,8 +9,6 @@ body:
|
||||
options:
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate feature requests
|
||||
required: true
|
||||
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: projects
|
||||
attributes:
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2024 CARIAD SE
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
name: 'Merge Queue CI Check Skipper'
|
||||
description: 'Outputs `skip-check` as `true` if this is running as part of merge queue checks and the same checks have already been executed in the PR itself.'
|
||||
inputs:
|
||||
secret:
|
||||
description: 'Optional GitHub Secret that can access branch protection rules using the administration:read permission'
|
||||
required: false
|
||||
outputs:
|
||||
skip-check:
|
||||
description: 'Skip Check (boolean)'
|
||||
value: ${{ steps.passed-checks.outputs.can-skip-checks }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract PR Number and Commit ID
|
||||
id: extract-pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const githubRef = process.env.GITHUB_REF;
|
||||
const regex = /^refs\/heads\/gh-readonly-queue\/([a-zA-Z0-9.\-_\/]+)\/pr-(\d+)-([a-f0-9]+)$/;
|
||||
|
||||
if (regex.test(githubRef)) {
|
||||
const [, targetBranchName, prNumber, commitId] = githubRef.match(regex);
|
||||
core.setOutput('targetBranchName', targetBranchName);
|
||||
core.setOutput('prNumber', prNumber);
|
||||
core.setOutput('commitId', commitId);
|
||||
} else {
|
||||
console.log(`GITHUB_REF is not a merge queue ref, setting CAN_SKIP_CHECKS to false: ${githubRef}`);
|
||||
core.exportVariable('CAN_SKIP_CHECKS', 'false');
|
||||
}
|
||||
|
||||
- name: Print PR Number and Target Commit ID
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Target Branch Name: ${{ steps.extract-pr-info.outputs.targetBranchName }}"
|
||||
echo "PR Number: ${{ steps.extract-pr-info.outputs.prNumber }}"
|
||||
echo "Target Commit ID: ${{ steps.extract-pr-info.outputs.commitId }}"
|
||||
|
||||
- name: Check if merge queue entry was enqueued as head of the queue
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
targetBranchHead=$(git rev-parse origin/${{ steps.extract-pr-info.outputs.targetBranchName }})
|
||||
if [[ "$targetBranchHead" != "${{ steps.extract-pr-info.outputs.commitId }}" ]]; then
|
||||
echo "'${{ steps.extract-pr-info.outputs.targetBranchName }}' branch commit ID does not match PR commit ID. This Merge Queue run was not head of the queue when it was enqueued. Setting CAN_SKIP_CHECKS to false."
|
||||
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "This merge queue entry is targeting '${{ steps.extract-pr-info.outputs.targetBranchName }}' directly."
|
||||
fi
|
||||
|
||||
- name: Get PR Branch
|
||||
id: get-pr-branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
prNumber=${{ steps.extract-pr-info.outputs.prNumber }}
|
||||
branchName=$(gh pr view ${prNumber} --json headRefName -q '.headRefName')
|
||||
echo "prBranch=$branchName" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Print PR Branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "PR Branch: ${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||
|
||||
- name: Check if PR branch contains the Merge Queue target commit ID
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
# Get the branch name from previous steps
|
||||
branch_name="origin/${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||
commit_id="${{ steps.extract-pr-info.outputs.commitId }}"
|
||||
|
||||
# Check if the branch history contains the commit
|
||||
if git branch -r --contains "$commit_id" | grep -q "$branch_name"; then
|
||||
echo "Branch '$branch_name' contains commit '$commit_id'. It is up to date with ${{ steps.extract-pr-info.outputs.targetBranchName }}."
|
||||
else
|
||||
echo "Branch '$branch_name' does not contain commit '$commit_id'. It is outdated. Setting CAN_SKIP_CHECKS to false."
|
||||
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Compare PR Branch with Current Branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
if git diff --quiet "origin/${{ steps.get-pr-branch.outputs.prBranch }}"; then
|
||||
echo "No differences found. PR branch is identical with this merge queue branch."
|
||||
else
|
||||
echo "Differences detected. PR branch has been updated after PR was added to merge queue. Setting CAN_SKIP_CHECKS to false."
|
||||
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Compute/publish skip result
|
||||
id: passed-checks
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SECRET: ${{ inputs.secret }}
|
||||
with:
|
||||
github-token: ${{ inputs.secret != '' && inputs.secret || github.token }}
|
||||
script: |
|
||||
if (process.env.CAN_SKIP_CHECKS == "false") {
|
||||
console.log("Setting CAN_SKIP_CHECKS to false");
|
||||
core.setOutput("can-skip-checks", false);
|
||||
return;
|
||||
}
|
||||
|
||||
const secretProvided = !!process.env.SECRET;
|
||||
if (!secretProvided) {
|
||||
console.log("secret input not set, assuming all checks have passed. Ensure 'Require status checks to pass before merging' is enabled, or provide a secret with administration:read.");
|
||||
core.setOutput("can-skip-checks", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: branchProtection } = await github.rest.repos.getBranchProtection({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
branch: "${{ steps.extract-pr-info.outputs.targetBranchName }}",
|
||||
});
|
||||
const requiredCheckNames = branchProtection.required_status_checks.contexts;
|
||||
console.log(`requiredCheckNames = ${requiredCheckNames}`);
|
||||
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: "refs/heads/${{ steps.get-pr-branch.outputs.prBranch }}",
|
||||
});
|
||||
|
||||
console.log(`checks.check_runs = ${checks.check_runs.map(check => `${check.status},${check.conclusion},${check.name};`)}`);
|
||||
const nonSuccessfulChecks = checks.check_runs.filter(check => check.status !== "completed" || check.conclusion !== "success");
|
||||
const nonSuccessfulCheckNames = nonSuccessfulChecks.map(check => check.name);
|
||||
console.log(`nonSuccessfulCheckNames = ${nonSuccessfulCheckNames}`);
|
||||
|
||||
const missingChecks = requiredCheckNames.filter(checkName => nonSuccessfulCheckNames.includes(checkName));
|
||||
|
||||
if (missingChecks.length > 0) {
|
||||
console.log(`Required checks not passed, cannot skip merge queue checks. Setting CAN_SKIP_CHECKS to false. Missing checks: ${missingChecks.join(', ')}`);
|
||||
core.setOutput("can-skip-checks", false);
|
||||
} else {
|
||||
console.log("No missing checks. Setting CAN_SKIP_CHECKS to true.");
|
||||
core.setOutput("can-skip-checks", true);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
name: API client release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/api-client-release.yml
|
||||
- packages/api-client/**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository_owner == 'modrinth' && github.ref == 'refs/heads/main'
|
||||
# npm Trusted Publishing requires a GitHub-hosted runner.
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FORCE_COLOR: 3
|
||||
PACKAGE_DIR: packages/api-client
|
||||
PACKAGE_NAME: '@modrinth/api-client'
|
||||
BUMP_TYPE: minor
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for api-client changes
|
||||
id: changes
|
||||
run: |
|
||||
if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if git diff --quiet "${{ github.event.before }}" "$GITHUB_SHA" -- "$PACKAGE_DIR"; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Enable Corepack
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore pnpm cache
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: pnpm install --frozen-lockfile --filter @modrinth/api-client...
|
||||
|
||||
- name: Resolve release version
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
id: version
|
||||
run: |
|
||||
CURRENT_VERSION_JSON="$(npm view "${PACKAGE_NAME}" version --json)"
|
||||
CURRENT_VERSION="$(
|
||||
jq -nr \
|
||||
--argjson version "$CURRENT_VERSION_JSON" \
|
||||
'if ($version | type) == "array" then $version[-1] else $version end'
|
||||
)"
|
||||
|
||||
NEXT_VERSION="$(
|
||||
jq -nr \
|
||||
--arg version "$CURRENT_VERSION" \
|
||||
--arg bump "$BUMP_TYPE" '
|
||||
def semver:
|
||||
capture("^(?<major>[0-9]+)\\.(?<minor>[0-9]+)\\.(?<patch>[0-9]+)$")
|
||||
| with_entries(.value |= tonumber);
|
||||
|
||||
($version | semver) as $current
|
||||
| if $bump == "major" then "\($current.major + 1).0.0"
|
||||
elif $bump == "minor" then "\($current.major).\($current.minor + 1).0"
|
||||
elif $bump == "patch" then "\($current.major).\($current.minor).\($current.patch + 1)"
|
||||
else error("Unsupported bump type: \($bump)")
|
||||
end
|
||||
'
|
||||
)"
|
||||
|
||||
PACKAGE_JSON="$(mktemp)"
|
||||
jq --tab --arg version "$NEXT_VERSION" '.version = $version' "$PACKAGE_DIR/package.json" > "$PACKAGE_JSON"
|
||||
mv "$PACKAGE_JSON" "$PACKAGE_DIR/package.json"
|
||||
|
||||
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "published_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build api-client
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: pnpm --filter @modrinth/api-client build
|
||||
|
||||
- name: Check package contents
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: packages/api-client
|
||||
run: pnpm pack --dry-run
|
||||
|
||||
- name: Publish api-client
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: packages/api-client
|
||||
run: pnpm publish --access public --provenance --no-git-checks
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Cancel PR Workflows on Merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1
|
||||
with:
|
||||
workflow_id: all
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ignore_sha: true
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
@@ -16,8 +16,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 💬 Post or update changelog comment
|
||||
uses: actions/github-script@v7
|
||||
- name: Post or update changelog comment
|
||||
uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -2,7 +2,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.43.1
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: crate-ci/typos@6ac2ebd1b93eade61faf7e12688ad87a073fea59 # v1.46.0
|
||||
|
||||
# see <https://github.com/influxdata/datafusion-udf-wasm/pull/275>
|
||||
tombi:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/install-action@v2
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2.75.29
|
||||
with:
|
||||
tool: tombi
|
||||
- run: tombi lint
|
||||
|
||||
@@ -2,7 +2,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -12,8 +12,8 @@ jobs:
|
||||
shear:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
- uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -3,33 +3,133 @@ name: daedalus-docker-build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- 'main'
|
||||
paths:
|
||||
- .github/workflows/daedalus-docker.yml
|
||||
- 'apps/daedalus_client/**'
|
||||
- 'packages/daedalus/**'
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- .github/workflows/daedalus-docker.yml
|
||||
- 'apps/daedalus_client/**'
|
||||
- 'packages/daedalus/**'
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
skip-if-clean:
|
||||
name: Skip if merge_queue produces no diff
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
internal: ${{ steps.check-internal.outputs.internal }}
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: 🧰 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Check if workflow runs on an internal branch
|
||||
id: check-internal
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" != "pull_request" ] || [ "$HEAD_REPO" = "$REPO" ]; then
|
||||
echo "internal=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "internal=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: ⚙️ Generate Docker image metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
- name: Merge Queue CI Check Skipper
|
||||
id: merge-queue-ci-skipper
|
||||
uses: ./.github/merge-queue-ci-skipper
|
||||
with:
|
||||
secret: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
- name: Check merge_group synthetic commit
|
||||
id: check
|
||||
run: |
|
||||
# PR mode: never skip
|
||||
if [ "${{ github.event_name }}" != "merge_group" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${{ steps.merge-queue-ci-skipper.outputs.skip-check }}" = "true" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
docker:
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
env:
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
with:
|
||||
rustflags: ''
|
||||
cache: false
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-daedalus-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Build daedalus_client
|
||||
run: cargo build --release --package daedalus_client
|
||||
|
||||
- name: Stage Docker context
|
||||
run: |
|
||||
mkdir -p apps/daedalus_client/docker-stage
|
||||
cp target/release/daedalus_client apps/daedalus_client/docker-stage/daedalus_client
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Generate Docker image metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
@@ -43,20 +143,19 @@ jobs:
|
||||
org.opencontainers.image.description=Modrinth game metadata query client
|
||||
org.opencontainers.image.licenses=MIT
|
||||
|
||||
- name: 🔑 Login to GitHub Packages
|
||||
uses: docker/login-action@v3
|
||||
- name: Login to GitHub Packages
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🔨 Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: ./apps/daedalus_client/docker-stage
|
||||
file: ./apps/daedalus_client/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
annotations: ${{ steps.docker_meta.outputs.annotations }}
|
||||
cache-from: type=registry,ref=ghcr.io/modrinth/daedalus:main
|
||||
cache-to: type=inline
|
||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||
annotations: ${{ steps.docker-meta.outputs.annotations }}
|
||||
|
||||
@@ -12,10 +12,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -21,16 +21,20 @@ on:
|
||||
type: string
|
||||
description: 'The environment to deploy to (staging-preview or production-preview)'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.environment || 'push' }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -63,14 +67,25 @@ jobs:
|
||||
echo "url=https://modrinth.com" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore pnpm cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
|
||||
- name: Inject build variables
|
||||
working-directory: ./apps/frontend
|
||||
@@ -99,7 +114,7 @@ jobs:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Create Sentry release and upload sourcemaps
|
||||
uses: getsentry/action-release@v3
|
||||
uses: getsentry/action-release@5657c9e888b4e2cc85f4d29143ea4131fde4a73a # v3.6.0
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: modrinth
|
||||
@@ -111,7 +126,7 @@ jobs:
|
||||
|
||||
- name: Deploy Cloudflare Worker
|
||||
id: wrangler
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
@@ -137,7 +152,7 @@ jobs:
|
||||
|
||||
- name: Upload deployment URL
|
||||
if: ${{ inputs.environment != '' }}
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: deployment-url-${{ inputs.environment }}
|
||||
path: deployment-url-${{ inputs.environment }}.txt
|
||||
|
||||
@@ -16,19 +16,77 @@ jobs:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/workflows/frontend-deploy.yml
|
||||
secrets: inherit
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.environment }}
|
||||
cancel-in-progress: true
|
||||
strategy:
|
||||
matrix:
|
||||
environment: [staging-preview, production-preview]
|
||||
with:
|
||||
environment: ${{ matrix.environment }}
|
||||
|
||||
deploy-storybook:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-storybook
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore pnpm cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./packages/ui
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Storybook
|
||||
working-directory: ./packages/ui
|
||||
run: pnpm run build-storybook
|
||||
|
||||
- name: Configure short SHA
|
||||
id: meta
|
||||
run: echo "sha_short=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Deploy Storybook preview
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./packages/ui
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }}
|
||||
|
||||
comment:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy
|
||||
needs: [deploy, deploy-storybook]
|
||||
steps:
|
||||
- name: Download deployment URLs
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: deployment-url-*
|
||||
merge-multiple: true
|
||||
@@ -44,10 +102,11 @@ jobs:
|
||||
|
||||
echo "staging-preview-url=$STAGING_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "production-preview-url=$PRODUCTION_PREVIEW_URL" >> $GITHUB_OUTPUT
|
||||
echo "storybook-preview-url=https://git-${GITHUB_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Find comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/find-comment@v3
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: fc
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
@@ -56,7 +115,7 @@ jobs:
|
||||
|
||||
- name: Comment deploy URL on PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
@@ -70,4 +129,5 @@ jobs:
|
||||
|-------------|-----|
|
||||
| staging | ${{ steps.urls.outputs.staging-preview-url }} |
|
||||
| production | ${{ steps.urls.outputs.production-preview-url }} |
|
||||
| storybook | ${{ steps.urls.outputs.storybook-preview-url }} |
|
||||
edit-mode: replace
|
||||
|
||||
@@ -51,14 +51,25 @@ jobs:
|
||||
CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install script dependencies
|
||||
run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Configure Git author
|
||||
id: git-author
|
||||
uses: MarcoIeni/git-config@v0.1
|
||||
uses: MarcoIeni/git-config@59144859caf016f8b817a2ac9b051578729173c4 # v0.1.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
|
||||
@@ -79,7 +90,7 @@ jobs:
|
||||
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download translations from Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
@@ -95,8 +106,11 @@ jobs:
|
||||
shell: bash
|
||||
run: sudo chown -R $USER:$USER .
|
||||
|
||||
- name: Prune stale ICU translations
|
||||
run: pnpm scripts i18n-icu-contract prune-local
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
title: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})'
|
||||
body-path: .github/templates/crowdin-pr.md
|
||||
|
||||
@@ -4,11 +4,14 @@ on:
|
||||
push:
|
||||
branches: ['main']
|
||||
paths:
|
||||
- '.github/workflows/i18n.push.yml'
|
||||
- '.github/workflows/i18n-push.yml'
|
||||
- 'apps/*/src/locales/en-US/**'
|
||||
- 'apps/*/locales/en-US/**'
|
||||
- 'packages/*/src/locales/en-US/**'
|
||||
- 'packages/*/locales/en-US/**'
|
||||
- 'scripts/i18n-icu-contract.ts'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'crowdin.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -53,9 +56,21 @@ jobs:
|
||||
CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Install script dependencies
|
||||
run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Query branch name
|
||||
id: branch-name
|
||||
@@ -68,7 +83,7 @@ jobs:
|
||||
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload translations to Crowdin
|
||||
uses: crowdin/github-action@v1
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
@@ -79,3 +94,9 @@ jobs:
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Clear stale ICU translations in Crowdin
|
||||
run: pnpm scripts i18n-icu-contract clear-crowdin-changed --base-ref HEAD^ --crowdin-branch "[${{ github.repository_owner }}.${{ github.event.repository.name }}] ${{ steps.branch-name.outputs.safe_branch_name }}"
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -3,7 +3,7 @@ name: docker-build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- 'main'
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- 'apps/labrinth/**'
|
||||
@@ -19,19 +19,122 @@ on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
skip-if-clean:
|
||||
name: Skip if merge_queue produces no diff
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
internal: ${{ steps.check-internal.outputs.internal }}
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: 🧰 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Check if workflow runs on an internal branch
|
||||
id: check-internal
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" != "pull_request" ] || [ "$HEAD_REPO" = "$REPO" ]; then
|
||||
echo "internal=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "internal=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: ⚙️ Generate Docker image metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
- name: Merge Queue CI Check Skipper
|
||||
id: merge-queue-ci-skipper
|
||||
uses: ./.github/merge-queue-ci-skipper
|
||||
with:
|
||||
secret: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
- name: Check merge_group synthetic commit
|
||||
id: check
|
||||
run: |
|
||||
# PR mode: never skip
|
||||
if [ "${{ github.event_name }}" != "merge_group" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${{ steps.merge-queue-ci-skipper.outputs.skip-check }}" = "true" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
docker:
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
env:
|
||||
SQLX_OFFLINE: 'true'
|
||||
GIT_HASH: ${{ github.sha }}
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
with:
|
||||
rustflags: ''
|
||||
cache: false
|
||||
|
||||
- name: Setup mold
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-labrinth-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Build labrinth
|
||||
run: cargo build --profile release-labrinth --package labrinth
|
||||
|
||||
- name: Stage Docker context
|
||||
run: |
|
||||
mkdir -p apps/labrinth/docker-stage
|
||||
cp target/release-labrinth/labrinth apps/labrinth/docker-stage/labrinth
|
||||
cp -r apps/labrinth/migrations apps/labrinth/docker-stage/migrations
|
||||
cp -r apps/labrinth/assets apps/labrinth/docker-stage/assets
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Generate Docker image metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
env:
|
||||
# GitHub Packages requires annotations metadata in at least the index descriptor to show them
|
||||
# up properly in its UI it seems, but it's not clear about it, because the docs refer to the
|
||||
@@ -49,22 +152,19 @@ jobs:
|
||||
org.opencontainers.image.description=Modrinth API
|
||||
org.opencontainers.image.licenses=AGPL-3.0-only
|
||||
|
||||
- name: 🔑 Login to GitHub Packages
|
||||
uses: docker/login-action@v3
|
||||
- name: Login to GitHub Packages
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 🔨 Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: ./apps/labrinth/docker-stage
|
||||
file: ./apps/labrinth/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
annotations: ${{ steps.docker_meta.outputs.annotations }}
|
||||
build-args: |
|
||||
GIT_HASH=${{ fromJSON(steps.docker_meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
cache-from: type=registry,ref=ghcr.io/modrinth/labrinth:main
|
||||
cache-to: type=inline
|
||||
push: true
|
||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||
annotations: ${{ steps.docker-meta.outputs.annotations }}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Prepare pnpm cache
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/prepare-pnpm-cache.yml
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm recursive install --frozen-lockfile
|
||||
@@ -30,46 +30,71 @@ on:
|
||||
- prod-with-staging-archon
|
||||
default: prod
|
||||
required: false
|
||||
app-version-override:
|
||||
description: Temporary app version override for updater testing
|
||||
type: string
|
||||
required: false
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
env:
|
||||
VITE_STRIPE_PUBLISHABLE_KEY: pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5
|
||||
# SCCACHE_DIR: '/mnt/sccache'
|
||||
# SCCACHE_CACHE_SIZE: '10G'
|
||||
# SCCACHE_MULTILEVEL_CHAIN: 'disk,s3'
|
||||
SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/'
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: 'sccache'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, windows-latest, ubuntu-latest]
|
||||
platform: [
|
||||
blacksmith-6vcpu-macos-26,
|
||||
blacksmith-8vcpu-windows-2025, # At time of writing, Windows 4 vCPU VMs don't seem to actually exist
|
||||
blacksmith-4vcpu-ubuntu-2404,
|
||||
]
|
||||
include:
|
||||
- platform: macos-latest
|
||||
- platform: blacksmith-6vcpu-macos-26
|
||||
artifact-target-name: universal-apple-darwin
|
||||
- platform: windows-latest
|
||||
- platform: blacksmith-8vcpu-windows-2025
|
||||
artifact-target-name: x86_64-pc-windows-msvc
|
||||
- platform: ubuntu-latest
|
||||
- platform: blacksmith-4vcpu-ubuntu-2404
|
||||
artifact-target-name: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
with:
|
||||
rustflags: ''
|
||||
target: ${{ startsWith(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Generate tauri-dev.conf.json
|
||||
shell: bash
|
||||
@@ -87,24 +112,28 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
version: v1 # cache key
|
||||
|
||||
- name: Setup Dasel
|
||||
uses: jaxxstorm/action-install-gh-release@v2.1.0
|
||||
uses: jaxxstorm/action-install-gh-release@25e24d2d23ae098373794ef1d6faecb48ee52da8 # v3.0.0
|
||||
with:
|
||||
repo: TomWright/dasel
|
||||
tag: v2.8.1
|
||||
extension-matching: disable
|
||||
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
rename-to: ${{ contains(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
chmod: 0755
|
||||
|
||||
- name: Set application version and environment
|
||||
shell: bash
|
||||
run: |
|
||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||
APP_VERSION="${{ inputs.app-version-override }}"
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||
fi
|
||||
BUILD_ENVIRONMENT="${{ inputs.environment || 'prod' }}"
|
||||
echo "Setting application version to $APP_VERSION"
|
||||
echo "Using environment $BUILD_ENVIRONMENT"
|
||||
@@ -115,13 +144,13 @@ jobs:
|
||||
cp "packages/app-lib/.env.${BUILD_ENVIRONMENT}" packages/app-lib/.env
|
||||
|
||||
- name: Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@v1.8
|
||||
uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set up Windows code signing
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
if: contains(matrix.platform, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then
|
||||
@@ -131,9 +160,10 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build macOS app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
|
||||
if: contains(matrix.platform, 'macos')
|
||||
env:
|
||||
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
@@ -145,8 +175,8 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: Build Linux app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
@@ -156,9 +186,9 @@ jobs:
|
||||
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
|
||||
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
|
||||
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
|
||||
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
|
||||
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
if: contains(matrix.platform, 'windows')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
@@ -167,7 +197,7 @@ jobs:
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: Upload app bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: App bundle (${{ matrix.artifact-target-name }})
|
||||
path: |
|
||||
|
||||
@@ -4,6 +4,10 @@ on:
|
||||
workflows: ['Modrinth App build']
|
||||
types: [completed]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release Modrinth App
|
||||
@@ -11,8 +15,7 @@ jobs:
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
startsWith(github.event.workflow_run.head_branch, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
VERSION_TAG: ${{ github.event.workflow_run.head_branch }}
|
||||
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
|
||||
@@ -21,10 +24,10 @@ jobs:
|
||||
LAUNCHER_FILES_BUCKET_BASE_URL: https://launcher-files.modrinth.com
|
||||
|
||||
steps:
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: 🔒 Verify ref is a tag
|
||||
- name: Verify ref is a tag
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
@@ -43,8 +46,8 @@ jobs:
|
||||
fi
|
||||
echo "Verified ${VERSION_TAG} is a tag pointing at ${HEAD_SHA}"
|
||||
|
||||
- name: 📥 Download Modrinth App artifacts
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
- name: Download Modrinth App artifacts
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
with:
|
||||
workflow: theseus-build.yml
|
||||
workflow_conclusion: success
|
||||
@@ -52,27 +55,15 @@ jobs:
|
||||
branch: ${{ env.VERSION_TAG }}
|
||||
use_unzip: true
|
||||
|
||||
- name: 📝 Extract app changelog
|
||||
- name: Extract app changelog
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ env.VERSION_TAG }}
|
||||
run: |
|
||||
node <<'EOF'
|
||||
const fs = require('fs');
|
||||
const version = process.env.VERSION.replace(/^v/, '');
|
||||
const src = fs.readFileSync('packages/blog/changelog.ts', 'utf8');
|
||||
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const re = new RegExp("product:\\s*'app',\\s*version:\\s*'" + escaped + "',\\s*body:\\s*`([\\s\\S]*?)`,\\s*\\}");
|
||||
const m = src.match(re);
|
||||
if (!m) {
|
||||
console.error(`No app changelog entry found for version ${version}`);
|
||||
process.exit(1);
|
||||
}
|
||||
fs.writeFileSync('release-notes.md', m[1]);
|
||||
console.log(`Extracted changelog for app ${version}:`);
|
||||
console.log(m[1]);
|
||||
EOF
|
||||
LAST_GITHUB_RELEASE_PUBLISHED_AT=$(gh api "repos/${{ github.repository }}/releases/latest" --jq '.published_at // ""' 2>/dev/null || true)
|
||||
LAST_GITHUB_RELEASE_PUBLISHED_AT="$LAST_GITHUB_RELEASE_PUBLISHED_AT" npx --yes tsx scripts/build-theseus-release-notes.ts
|
||||
|
||||
- name: 🛠️ Generate version manifest
|
||||
- name: Generate version manifest
|
||||
run: |
|
||||
# Reference: https://tauri.app/plugin/updater/#server-support
|
||||
jq -nc \
|
||||
@@ -117,7 +108,7 @@ jobs:
|
||||
echo "Generated manifest for version ${VERSION_TAG}:"
|
||||
cat updates.json
|
||||
|
||||
- name: 📤 Upload release artifacts
|
||||
- name: Upload release artifacts
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.LAUNCHER_FILES_BUCKET_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LAUNCHER_FILES_BUCKET_SECRET_ACCESS_KEY }}
|
||||
@@ -152,7 +143,7 @@ jobs:
|
||||
|
||||
aws s3 cp updates.json "s3://${AWS_BUCKET}"
|
||||
|
||||
- name: 🏷️ Create GitHub release
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
|
||||
+132
-30
@@ -8,10 +8,63 @@ on:
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
skip-if-clean:
|
||||
name: Skip if merge_queue produces no diff
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.check.outputs.skip }}
|
||||
internal: ${{ steps.check-internal.outputs.internal }}
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Check if workflow runs on an internal branch
|
||||
id: check-internal
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" != "pull_request" ] || [ "$HEAD_REPO" = "$REPO" ]; then
|
||||
echo "internal=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "internal=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Merge Queue CI Check Skipper
|
||||
id: merge-queue-ci-skipper
|
||||
uses: ./.github/merge-queue-ci-skipper
|
||||
with:
|
||||
secret: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
- name: Check merge_group synthetic commit
|
||||
id: check
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
SKIP_CHECK: ${{ steps.merge-queue-ci-skipper.outputs.skip-check }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" != "merge_group" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$SKIP_CHECK" = "true" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Lint and Test
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
|
||||
env:
|
||||
# Ensure pnpm output is colored in GitHub Actions logs
|
||||
@@ -23,59 +76,107 @@ jobs:
|
||||
# since we don't want warnings to become errors
|
||||
# while developing)
|
||||
RUSTFLAGS: -Dwarnings
|
||||
# sccache config (only populated for internal branches; secrets aren't
|
||||
# available to forked PRs, and blacksmith stickydisk requires a
|
||||
# blacksmith runner)
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_MULTILEVEL_CHAIN: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'disk,s3' || '' }}
|
||||
SCCACHE_S3_KEY_PREFIX: ${{ needs.skip-if-clean.outputs.internal == 'true' && format('{0}/', github.repository) || '' }}
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
|
||||
steps:
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: 🧰 Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
- name: Install build dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
version: v1 # cache key
|
||||
|
||||
- name: 🧰 Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🧰 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: 🧰 Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore pnpm cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
with:
|
||||
rustflags: ''
|
||||
components: clippy, rustfmt
|
||||
cache: false
|
||||
|
||||
- name: 🧰 Setup nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-turbo-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Setup binstall
|
||||
uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
|
||||
|
||||
- name: Setup nextest
|
||||
run: cargo binstall --no-confirm --secure cargo-nextest@0.9.133
|
||||
|
||||
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||
# back to a cached cargo install
|
||||
- name: 🧰 Setup cargo-sqlx
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
- name: Setup cargo-sqlx
|
||||
uses: taiki-e/cache-cargo-install-action@f9eed3e4680f27610dc6d8c67be1b88593f7dade # v3.0.6
|
||||
with:
|
||||
tool: sqlx-cli
|
||||
tool: sqlx-cli@0.8.6
|
||||
locked: false
|
||||
no-default-features: true
|
||||
features: rustls,postgres
|
||||
|
||||
- name: 💨 Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@v1.8
|
||||
- name: Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14
|
||||
|
||||
- name: 🧰 Install dependencies
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ⚙️ Set app environment
|
||||
- name: Set app environment
|
||||
working-directory: packages/app-lib
|
||||
run: cp .env.staging .env
|
||||
|
||||
# check if labrinth tests will actually run (cache miss)
|
||||
- name: 🔍 Check if labrinth tests need to run
|
||||
- name: Check if labrinth tests need to run
|
||||
id: check-labrinth
|
||||
run: |
|
||||
LABRINTH_TEST_STATUS=$(pnpm turbo run test --filter=@modrinth/labrinth --dry-run=json | jq -r '.tasks[] | select(.task == "test") | .cache.status')
|
||||
@@ -86,21 +187,22 @@ jobs:
|
||||
echo "needs_services=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: ⚙️ Start services
|
||||
- name: Start services
|
||||
if: steps.check-labrinth.outputs.needs_services == 'true'
|
||||
run: docker compose up --wait
|
||||
|
||||
- name: ⚙️ Setup labrinth environment and database
|
||||
- name: Setup labrinth environment and database
|
||||
if: steps.check-labrinth.outputs.needs_services == 'true'
|
||||
working-directory: apps/labrinth
|
||||
run: |
|
||||
cp .env.local .env
|
||||
sqlx database setup
|
||||
|
||||
- name: 🔍 Lint and test
|
||||
- name: Lint and test
|
||||
run: pnpm run ci
|
||||
|
||||
- name: 🔍 Verify intl:extract has been run
|
||||
- name: Verify intl:extract has been run
|
||||
run: |
|
||||
pnpm turbo run intl:extract --force
|
||||
git diff --exit-code --color */*/src/locales/en-US/index.json
|
||||
pnpm scripts i18n-icu-contract prune-local --check
|
||||
git diff --exit-code --color */*/src/locales/*/index.json
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,2 +1,11 @@
|
||||
strict-peer-dependencies=false
|
||||
auto-install-peers=true
|
||||
public-hoist-pattern[]=prettier-plugin-*
|
||||
public-hoist-pattern[]=@prettier/plugin-*
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=@eslint/*
|
||||
public-hoist-pattern[]=eslint-plugin-*
|
||||
public-hoist-pattern[]=@nuxt/eslint-config
|
||||
public-hoist-pattern[]=typescript-eslint
|
||||
public-hoist-pattern[]=vue-eslint-parser
|
||||
public-hoist-pattern[]=globals
|
||||
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
||||
"files.insertFinalNewline": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "always"
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[vue]": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
See [CLAUDE.md](./CLAUDE.md) for all project instructions and guidelines.
|
||||
@@ -64,7 +64,7 @@ The website and app `prepr` commands
|
||||
|
||||
Each project may have its own `CLAUDE.md` with detailed instructions:
|
||||
|
||||
- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API
|
||||
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
|
||||
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
|
||||
|
||||
## Code Guidelines
|
||||
|
||||
Generated
+468
-55
@@ -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,10 +72,10 @@ dependencies = [
|
||||
"brotli",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
@@ -226,9 +226,9 @@ dependencies = [
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"cookie 0.16.2",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"encoding_rs",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"impl-more",
|
||||
@@ -914,6 +914,18 @@ dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "1.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-creds"
|
||||
version = "0.39.0"
|
||||
@@ -964,6 +976,295 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-runtime"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand 2.3.0",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.0.1",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tracing",
|
||||
"uuid 1.18.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-s3"
|
||||
version = "1.122.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94c2ca0cba97e8e279eb6c0b2d0aa10db5959000e602ab2b7c02de6b85d4c19b"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-sigv4",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-checksums",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-json",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-smithy-xml",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand 2.3.0",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"lru",
|
||||
"percent-encoding",
|
||||
"regex-lite",
|
||||
"sha2",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sigv4"
|
||||
version = "1.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"percent-encoding",
|
||||
"sha2",
|
||||
"time",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-async"
|
||||
version = "1.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-checksums"
|
||||
version = "0.64.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddcf418858f9f3edd228acb8759d77394fed7531cce78d02bdda499025368439"
|
||||
dependencies = [
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"crc-fast",
|
||||
"hex",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"md-5",
|
||||
"pin-project-lite",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-eventstream"
|
||||
version = "0.60.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"crc32fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http"
|
||||
version = "0.63.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc"
|
||||
dependencies = [
|
||||
"aws-smithy-eventstream",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-http-client"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"h2 0.3.27",
|
||||
"h2 0.4.12",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper 1.7.0",
|
||||
"hyper-rustls 0.24.2",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"pin-project-lite",
|
||||
"rustls 0.21.12",
|
||||
"rustls 0.23.32",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-json"
|
||||
version = "0.62.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979"
|
||||
dependencies = [
|
||||
"aws-smithy-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-observability"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d"
|
||||
dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http",
|
||||
"aws-smithy-http-client",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"fastrand 2.3.0",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-runtime-api"
|
||||
version = "1.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-types",
|
||||
"bytes",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-types"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"bytes",
|
||||
"bytes-utils",
|
||||
"futures-core",
|
||||
"http 0.2.12",
|
||||
"http 1.3.1",
|
||||
"http-body 0.4.6",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"num-integer",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"ryu",
|
||||
"serde",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-smithy-xml"
|
||||
version = "0.60.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57"
|
||||
dependencies = [
|
||||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-types"
|
||||
version = "1.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"rustc_version",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.8"
|
||||
@@ -1052,6 +1353,16 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64-simd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.0"
|
||||
@@ -1372,6 +1683,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes-utils"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
version = "1.5.0"
|
||||
@@ -1572,18 +1893,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cidre"
|
||||
version = "0.11.3"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc0eb4d7faf9f94493eaf7e7c0534dee2dfa9642353382039572abb4e0e56e9"
|
||||
checksum = "0c903ff1729987d29e07295d2c5841993db25ff86cca43bee04505878d3ec1a7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cidre-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cidre-macros"
|
||||
version = "0.3.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82bc2f84c0baaa09299da3a03864491549685912c1e338a54211e00589dc1e4c"
|
||||
checksum = "6facfeffa8b9792dc014bb4e65e364d13518b74e06783d56249810a3e48cfad7"
|
||||
|
||||
[[package]]
|
||||
name = "cityhash-rs"
|
||||
@@ -1902,6 +2224,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"
|
||||
@@ -2016,6 +2347,18 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc-fast"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d"
|
||||
dependencies = [
|
||||
"crc",
|
||||
"digest",
|
||||
"rustversion",
|
||||
"spin 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -2450,21 +2793,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",
|
||||
]
|
||||
@@ -2714,29 +3059,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elasticsearch"
|
||||
version = "9.1.0-alpha.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12bb303aa6e1d28c0c86b6fbfe484fd0fd3f512629aeed1ac4f6b85f81d9834a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"dyn-clone",
|
||||
"flate2",
|
||||
"lazy_static",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"reqwest 0.12.24",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"tokio",
|
||||
"url",
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
@@ -3139,7 +3461,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin",
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3154,6 +3476,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -3805,7 +4133,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3813,6 +4141,11 @@ name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -4484,7 +4817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.0",
|
||||
"hashbrown 0.15.5",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -4581,9 +4914,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "io-uring"
|
||||
version = "0.7.10"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
|
||||
checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cfg-if",
|
||||
@@ -4902,6 +5235,7 @@ dependencies = [
|
||||
"async-minecraft-ping",
|
||||
"async-stripe",
|
||||
"async-trait",
|
||||
"aws-sdk-s3",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.4",
|
||||
"bytes",
|
||||
@@ -4914,11 +5248,10 @@ dependencies = [
|
||||
"const_format",
|
||||
"dashmap",
|
||||
"deadpool-redis",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"dotenv-build",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"elasticsearch",
|
||||
"eyre",
|
||||
"futures",
|
||||
"futures-util",
|
||||
@@ -4941,12 +5274,12 @@ dependencies = [
|
||||
"paste",
|
||||
"path-util",
|
||||
"prometheus",
|
||||
"quick-xml 0.38.3",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"redis",
|
||||
"regex",
|
||||
"reqwest 0.12.24",
|
||||
"rust-s3",
|
||||
"rust_decimal",
|
||||
"rust_iso3166",
|
||||
"rustls 0.23.32",
|
||||
@@ -5005,7 +5338,7 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin",
|
||||
"spin 0.9.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5227,6 +5560,15 @@ dependencies = [
|
||||
"imgref",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
@@ -5515,7 +5857,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 +5923,7 @@ dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"reqwest 0.12.24",
|
||||
"rust_decimal",
|
||||
"rust_iso3166",
|
||||
@@ -6468,6 +6810,12 @@ dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.3"
|
||||
@@ -6592,7 +6940,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",
|
||||
@@ -7811,15 +8159,20 @@ dependencies = [
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.7.0",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.23.32",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -8161,6 +8514,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.32",
|
||||
"rustls-native-certs 0.8.1",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.103.7",
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
@@ -9059,6 +9439,12 @@ dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -9274,7 +9660,7 @@ dependencies = [
|
||||
name = "sqlx-tracing"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"futures",
|
||||
"opentelemetry",
|
||||
"opentelemetry-testing",
|
||||
@@ -9909,9 +10295,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
|
||||
version = "2.10.1"
|
||||
source = "git+https://github.com/modrinth/plugins-workspace?rev=0d30f2aa28ec668ce187d527da1c475da3c01cbc#0d30f2aa28ec668ce187d527da1c475da3c01cbc"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
@@ -9923,7 +10308,8 @@ dependencies = [
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.12.24",
|
||||
"reqwest 0.13.2",
|
||||
"rustls 0.23.32",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -10147,7 +10533,7 @@ dependencies = [
|
||||
"daedalus",
|
||||
"dashmap",
|
||||
"data-url",
|
||||
"derive_more 2.0.1",
|
||||
"derive_more 2.1.1",
|
||||
"dirs",
|
||||
"discord-rich-presence",
|
||||
"dotenvy",
|
||||
@@ -10155,9 +10541,11 @@ dependencies = [
|
||||
"either",
|
||||
"encoding_rs",
|
||||
"enumset",
|
||||
"eyre",
|
||||
"flate2",
|
||||
"fs4",
|
||||
"futures",
|
||||
"futures-lite 2.6.1",
|
||||
"heck 0.5.0",
|
||||
"hickory-resolver 0.25.2",
|
||||
"indicatif",
|
||||
@@ -10209,6 +10597,8 @@ version = "1.0.0-local"
|
||||
dependencies = [
|
||||
"async_zip",
|
||||
"chrono",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"daedalus",
|
||||
"dashmap",
|
||||
"either",
|
||||
@@ -10216,6 +10606,7 @@ dependencies = [
|
||||
"hyper 1.7.0",
|
||||
"hyper-util",
|
||||
"native-dialog",
|
||||
"objc2-app-kit",
|
||||
"paste",
|
||||
"path-util",
|
||||
"serde",
|
||||
@@ -10241,6 +10632,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"uuid 1.18.1",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
@@ -11291,6 +11683,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
|
||||
[[package]]
|
||||
name = "vsimd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
@@ -11590,6 +11988,15 @@ dependencies = [
|
||||
"libwebp-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -12357,6 +12764,12 @@ version = "0.8.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
||||
|
||||
[[package]]
|
||||
name = "xmlparser"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
|
||||
|
||||
[[package]]
|
||||
name = "yaserde"
|
||||
version = "0.12.0"
|
||||
|
||||
+15
-5
@@ -44,6 +44,11 @@ async-tungstenite = { version = "0.31.0", default-features = false, features = [
|
||||
] }
|
||||
async-walkdir = "2.1.0"
|
||||
async_zip = "0.0.18"
|
||||
aws-sdk-s3 = { version = "=1.122.0", default-features = false, features = [
|
||||
"default-https-client",
|
||||
"rt-tokio",
|
||||
"rustls",
|
||||
] }
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.4"
|
||||
bytemuck = "1.24.0"
|
||||
@@ -51,7 +56,7 @@ bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.42"
|
||||
cidre = { version = "0.11.3", default-features = false, features = [
|
||||
cidre = { version = "0.15.0", default-features = false, features = [
|
||||
"macos_15_0"
|
||||
] }
|
||||
clap = "4.5.48"
|
||||
@@ -59,12 +64,14 @@ clickhouse = "0.14.0"
|
||||
color-eyre = "0.6.5"
|
||||
color-thief = "0.2.2"
|
||||
const_format = "0.2.34"
|
||||
core-foundation = "0.10.1"
|
||||
core-graphics = "0.24.0"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
darling = { version = "0.23" }
|
||||
dashmap = "6.1.0"
|
||||
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"
|
||||
@@ -72,13 +79,13 @@ dotenv-build = "0.1.1"
|
||||
dotenvy = "0.15.7"
|
||||
dunce = "1.0.5"
|
||||
either = "1.15.0"
|
||||
elasticsearch = "9.1.0-alpha.1"
|
||||
encoding_rs = "0.8.35"
|
||||
enumset = "1.1.10"
|
||||
eyre = "0.6.12"
|
||||
flate2 = "1.1.4"
|
||||
fs4 = { version = "0.13.1", default-features = false }
|
||||
futures = "0.3.31"
|
||||
futures-lite = "2.6.1"
|
||||
futures-util = "0.3.31"
|
||||
heck = "0.5.0"
|
||||
hex = "0.4.3"
|
||||
@@ -119,6 +126,7 @@ murmur2 = "0.1.0"
|
||||
native-dialog = "0.9.2"
|
||||
notify = { version = "8.2.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
p256 = "0.13.2"
|
||||
parking_lot = "0.12.5"
|
||||
paste = "1.0.15"
|
||||
@@ -184,7 +192,7 @@ tauri-plugin-http = "2.5.7"
|
||||
tauri-plugin-opener = "2.5.0"
|
||||
tauri-plugin-os = "2.3.1"
|
||||
tauri-plugin-single-instance = "2.3.4"
|
||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||
tauri-plugin-updater = { git = "https://github.com/modrinth/plugins-workspace", rev = "0d30f2aa28ec668ce187d527da1c475da3c01cbc", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"zip",
|
||||
] }
|
||||
@@ -258,6 +266,7 @@ read_zero_byte_vec = "warn"
|
||||
redundant_clone = "warn"
|
||||
redundant_feature_names = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
result_large_err = "allow"
|
||||
todo = "warn"
|
||||
too_many_arguments = "allow"
|
||||
uninlined_format_args = "warn"
|
||||
@@ -273,10 +282,11 @@ opt-level = "s" # Optimize for binary size
|
||||
strip = true # Remove debug symbols
|
||||
lto = true # Enables link to optimizations
|
||||
panic = "abort" # Strip expensive panic clean-up logic
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
|
||||
# Specific profile for labrinth production builds
|
||||
[profile.release-labrinth]
|
||||
inherits = "release"
|
||||
opt-level = 2
|
||||
strip = false # Keep debug symbols for Sentry
|
||||
lto = "thin" # Enable LTO but keep compile times reasonable
|
||||
panic = "unwind" # Don't exit the whole app on panic in production
|
||||
|
||||
@@ -15,10 +15,10 @@ If you're not a developer and you've stumbled upon this repository, you can acce
|
||||
|
||||
## Development
|
||||
|
||||
This repository contains two primary packages. For detailed development information, please refer to their respective READMEs:
|
||||
This repository contains two primary packages. For detailed development information, please refer to their respective guides:
|
||||
|
||||
- [Web Interface](apps/frontend/README.md)
|
||||
- [Desktop App](apps/app/README.md)
|
||||
- [Website frontend](https://docs.modrinth.com/contributing/knossos/)
|
||||
- [Desktop app](https://docs.modrinth.com/contributing/theseus/)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/app-frontend",
|
||||
"test": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -19,7 +20,7 @@
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@sfirew/minecraft-motd-parser": "^1.1.6",
|
||||
"@tanstack/vue-query": "^5.90.7",
|
||||
"@tanstack/vue-query": "5.90.7",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
@@ -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",
|
||||
|
||||
+567
-262
File diff suppressed because it is too large
Load Diff
@@ -180,13 +180,6 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
input[type='button'] {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: 2px solid transparent;
|
||||
}
|
||||
|
||||
@import '@modrinth/assets/omorphia.scss';
|
||||
|
||||
input {
|
||||
|
||||
@@ -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>
|
||||
@@ -149,7 +149,7 @@ const handleOptionsClick = async (args) => {
|
||||
break
|
||||
case 'edit':
|
||||
await router.push({
|
||||
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
||||
path: `/instance/${encodeURIComponent(args.item.path)}`,
|
||||
})
|
||||
break
|
||||
case 'duplicate':
|
||||
|
||||
@@ -1,81 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="mode !== 'isolated'"
|
||||
ref="button"
|
||||
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||
:class="{ expanded: mode === 'expanded' }"
|
||||
@click="toggleMenu"
|
||||
v-if="accounts.length === 0"
|
||||
class="flex flex-col gap-3 bg-button-bg border border-solid border-surface-5 rounded-xl p-3 mt-2"
|
||||
>
|
||||
<Avatar
|
||||
size="36px"
|
||||
:src="
|
||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col w-full">
|
||||
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
||||
<span class="text-secondary text-xs">Minecraft account</span>
|
||||
</div>
|
||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||
<span>{{ formatMessage(messages.notSignedIn) }}</span>
|
||||
<ButtonStyled color="brand">
|
||||
<button color="primary" :disabled="loginDisabled" @click="login()">
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
{{ formatMessage(messages.signInToMinecraft) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<Card
|
||||
v-if="showCard || mode === 'isolated'"
|
||||
ref="card"
|
||||
class="account-card"
|
||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||
>
|
||||
<div v-if="selectedAccount" class="selected account">
|
||||
<Avatar size="xs" :src="avatarUrl" />
|
||||
<div>
|
||||
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||
<p>Selected</p>
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="'Log out'"
|
||||
icon-only
|
||||
color="raised"
|
||||
@click="logout(selectedAccount.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="logged-out account">
|
||||
<h4>Not signed in</h4>
|
||||
<Button
|
||||
v-tooltip="'Log in'"
|
||||
:disabled="loginDisabled"
|
||||
icon-only
|
||||
color="primary"
|
||||
@click="login()"
|
||||
>
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||
<Button class="option account" @click="setAccount(account)">
|
||||
<Avatar :src="getAccountAvatarUrl(account)" class="icon" />
|
||||
<p>{{ account.profile.name }}</p>
|
||||
</Button>
|
||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
<Accordion
|
||||
v-else
|
||||
class="w-full mt-2 bg-button-bg border border-solid border-surface-5 rounded-xl overflow-clip"
|
||||
button-class="button-base w-full bg-transparent px-3 py-2 border-0 cursor-pointer"
|
||||
:open-by-default="false"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex gap-2 w-full min-w-0">
|
||||
<Avatar
|
||||
size="36px"
|
||||
:src="
|
||||
selectedAccount
|
||||
? avatarUrl
|
||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col items-start w-full min-w-0">
|
||||
<span class="truncate w-full text-left">{{
|
||||
selectedAccount ? selectedAccount.profile.name : formatMessage(messages.selectAccount)
|
||||
}}</span>
|
||||
<span class="text-secondary text-xs">{{ formatMessage(messages.minecraftAccount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button v-if="accounts.length > 0" @click="login()">
|
||||
<PlusIcon />
|
||||
Add account
|
||||
</Button>
|
||||
</Card>
|
||||
</transition>
|
||||
</template>
|
||||
<div class="bg-button-bg pt-1 pb-2 border border-solid border-surface-5">
|
||||
<template v-if="accounts.length > 0">
|
||||
<div v-for="account in accounts" :key="account.profile.id" class="flex gap-1 items-center">
|
||||
<button
|
||||
class="flex items-center flex-shrink flex-grow overflow-clip gap-2 p-2 border-0 bg-transparent cursor-pointer button-base min-w-0"
|
||||
@click="setAccount(account)"
|
||||
>
|
||||
<RadioButtonCheckedIcon
|
||||
v-if="selectedAccount && selectedAccount.profile.id === account.profile.id"
|
||||
class="w-5 h-5 text-brand shrink-0"
|
||||
/>
|
||||
<RadioButtonIcon v-else class="w-5 h-5 text-secondary shrink-0" />
|
||||
<Avatar :src="getAccountAvatarUrl(account)" size="24px" />
|
||||
<p
|
||||
class="m-0 truncate min-w-0"
|
||||
:class="
|
||||
selectedAccount && selectedAccount.profile.id === account.profile.id
|
||||
? 'text-contrast font-semibold'
|
||||
: 'text-primary'
|
||||
"
|
||||
>
|
||||
{{ account.profile.name }}
|
||||
</p>
|
||||
</button>
|
||||
<ButtonStyled circular color="red" color-fill="none" hover-color-fill="background">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.removeAccount)"
|
||||
class="mr-2"
|
||||
@click="logout(account.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2 px-2 pt-2">
|
||||
<ButtonStyled v-if="accounts.length > 0" class="w-full">
|
||||
<button :disabled="loginDisabled" @click="login()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addAccount) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DropdownIcon, LogInIcon, PlusIcon, SpinnerIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LogInIcon,
|
||||
PlusIcon,
|
||||
RadioButtonCheckedIcon,
|
||||
RadioButtonIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import {
|
||||
@@ -87,39 +113,47 @@ import {
|
||||
} from '@/helpers/auth'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import type { Skin } from '@/helpers/skins'
|
||||
import { get_available_skins } from '@/helpers/skins'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'normal',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
change: []
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
type MinecraftCredential = {
|
||||
profile: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = ref({})
|
||||
const accounts: Ref<MinecraftCredential[]> = ref([])
|
||||
const loginDisabled = ref(false)
|
||||
const defaultUser = ref()
|
||||
const equippedSkin = ref(null)
|
||||
const headUrlCache = ref(new Map())
|
||||
const defaultUser = ref<string | undefined>()
|
||||
const equippedSkin = ref<Skin | null>(null)
|
||||
const headUrlCache = ref(new Map<string, string>())
|
||||
|
||||
async function refreshValues() {
|
||||
defaultUser.value = await get_default_user().catch(handleError)
|
||||
accounts.value = await users().catch(handleError)
|
||||
const userList = await users().catch(handleError)
|
||||
accounts.value = Array.isArray(userList) ? [...userList] : []
|
||||
accounts.value.sort((a, b) => (a.profile?.name ?? '').localeCompare(b.profile?.name ?? ''))
|
||||
|
||||
try {
|
||||
const skins = await get_available_skins()
|
||||
equippedSkin.value = skins.find((skin) => skin.is_equipped)
|
||||
equippedSkin.value = skins.find((skin) => skin.is_equipped) ?? null
|
||||
|
||||
if (equippedSkin.value) {
|
||||
try {
|
||||
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
|
||||
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
|
||||
headUrlCache.value = new Map(headUrlCache.value).set(
|
||||
equippedSkin.value.texture_key,
|
||||
headUrl,
|
||||
)
|
||||
} catch (error) {
|
||||
console.warn('Failed to get head render for equipped skin:', error)
|
||||
}
|
||||
@@ -129,19 +163,32 @@ async function refreshValues() {
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginDisabled(value) {
|
||||
async function setEquippedSkin(skin: Skin) {
|
||||
equippedSkin.value = skin
|
||||
|
||||
try {
|
||||
const headUrl = await getPlayerHeadUrl(skin)
|
||||
headUrlCache.value = new Map(headUrlCache.value).set(skin.texture_key, headUrl)
|
||||
} catch (error) {
|
||||
console.warn('Failed to get head render for equipped skin:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginDisabled(value: boolean) {
|
||||
loginDisabled.value = value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refreshValues,
|
||||
setEquippedSkin,
|
||||
setLoginDisabled,
|
||||
loginDisabled,
|
||||
})
|
||||
|
||||
await refreshValues()
|
||||
|
||||
const displayAccounts = computed(() =>
|
||||
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
@@ -158,7 +205,7 @@ const avatarUrl = computed(() => {
|
||||
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
})
|
||||
|
||||
function getAccountAvatarUrl(account) {
|
||||
function getAccountAvatarUrl(account: MinecraftCredential) {
|
||||
if (
|
||||
account.profile.id === selectedAccount.value?.profile?.id &&
|
||||
equippedSkin.value?.texture_key
|
||||
@@ -171,13 +218,10 @@ function getAccountAvatarUrl(account) {
|
||||
return `https://mc-heads.net/avatar/${account.profile.id}/128`
|
||||
}
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
|
||||
async function setAccount(account) {
|
||||
async function setAccount(account: MinecraftCredential) {
|
||||
defaultUser.value = account.profile.id
|
||||
await set_default_user(account.profile.id).catch(handleError)
|
||||
await refreshValues()
|
||||
emit('change')
|
||||
}
|
||||
|
||||
@@ -187,292 +231,57 @@ async function login() {
|
||||
|
||||
if (loggedIn) {
|
||||
await setAccount(loggedIn)
|
||||
await refreshValues()
|
||||
}
|
||||
|
||||
trackEvent('AccountLogIn')
|
||||
loginDisabled.value = false
|
||||
}
|
||||
|
||||
const logout = async (id) => {
|
||||
async function logout(id: string) {
|
||||
await remove_user(id).catch(handleError)
|
||||
await refreshValues()
|
||||
if (!selectedAccount.value && accounts.value.length > 0) {
|
||||
await setAccount(accounts.value[0])
|
||||
await refreshValues()
|
||||
} else {
|
||||
emit('change')
|
||||
}
|
||||
trackEvent('AccountLogOut')
|
||||
}
|
||||
|
||||
const showCard = ref(false)
|
||||
const card = ref(null)
|
||||
const button = ref(null)
|
||||
const handleClickOutside = (event) => {
|
||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||
if (
|
||||
card.value &&
|
||||
card.value.$el !== event.target &&
|
||||
!elements.includes(card.value.$el) &&
|
||||
!button.value.contains(event.target)
|
||||
) {
|
||||
toggleMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMenu(override = true) {
|
||||
if (showCard.value || !override) {
|
||||
showCard.value = false
|
||||
} else {
|
||||
showCard.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const unlisten = await process_listener(async (e) => {
|
||||
if (e.event === 'launched') {
|
||||
await refreshValues()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
notSignedIn: {
|
||||
id: 'minecraft-account.not-signed-in',
|
||||
defaultMessage: 'Not signed in',
|
||||
},
|
||||
addAccount: {
|
||||
id: 'minecraft-account.add-account',
|
||||
defaultMessage: 'Add account',
|
||||
},
|
||||
removeAccount: {
|
||||
id: 'minecraft-account.remove-account',
|
||||
defaultMessage: 'Remove account',
|
||||
},
|
||||
selectAccount: {
|
||||
id: 'minecraft-account.select-account',
|
||||
defaultMessage: 'Select account',
|
||||
},
|
||||
minecraftAccount: {
|
||||
id: 'minecraft-account.label',
|
||||
defaultMessage: 'Minecraft account',
|
||||
},
|
||||
signInToMinecraft: {
|
||||
id: 'minecraft-account.sign-in',
|
||||
defaultMessage: 'Sign in to Minecraft',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.selected {
|
||||
background: var(--color-brand-highlight);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-contrast);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logged-out {
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.account {
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
h4,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.account-card {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.5rem;
|
||||
right: 2rem;
|
||||
z-index: 11;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-divider);
|
||||
width: max-content;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
border-top-right-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
border-top-right-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
left: 13.5rem;
|
||||
}
|
||||
|
||||
&.isolated {
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.accounts-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.account-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.option {
|
||||
width: calc(100% - 2.25rem);
|
||||
background: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
box-shadow: none;
|
||||
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
--size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.account-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
vertical-align: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
translate 0.25s ease,
|
||||
scale 0.25s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
translate: 0 -2rem;
|
||||
scale: 0.9;
|
||||
}
|
||||
|
||||
.avatar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-base);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--radius-md);
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
|
||||
&.expanded {
|
||||
border: 1px solid var(--color-divider);
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
margin: auto 0 auto 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 6rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.accounts-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
background-color: white !important;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-lg);
|
||||
align-items: center;
|
||||
padding: var(--gap-xl);
|
||||
|
||||
.modal-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
width: 100%;
|
||||
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-xs);
|
||||
align-items: center;
|
||||
|
||||
.code {
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--radius-md);
|
||||
border: solid 1px var(--color-button-bg);
|
||||
font-family: var(--mono-font);
|
||||
letter-spacing: var(--gap-md);
|
||||
color: var(--color-contrast);
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
padding: var(--gap-sm) 0 var(--gap-sm) var(--gap-md);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.code {
|
||||
color: var(--color-brand);
|
||||
padding: 0.05rem 0.1rem;
|
||||
// row not column
|
||||
display: flex;
|
||||
|
||||
.card {
|
||||
background: var(--color-base);
|
||||
color: var(--color-contrast);
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<div class="flex gap-2 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>
|
||||
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
|
||||
<button
|
||||
v-if="showUpdatePill"
|
||||
type="button"
|
||||
class="!h-[34px] overflow-hidden text-sm !transition-[width,opacity,transform,background-color,color,filter] !duration-200 ease-out"
|
||||
:class="[
|
||||
updatePillWidthClass,
|
||||
{
|
||||
'update-pill-ready-hidden': finishedDownloading && !animateReadyPill,
|
||||
'update-pill-ready-visible': finishedDownloading && animateReadyPill,
|
||||
},
|
||||
]"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||
</span>
|
||||
<span v-else>{{ updateLabel }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<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,
|
||||
RefreshCwIcon,
|
||||
StarIcon,
|
||||
StopCircleIcon,
|
||||
TerminalSquareIcon,
|
||||
UnplugIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
injectPopupNotificationManager,
|
||||
type PopupNotification,
|
||||
type PopupNotificationProgressItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { Dropdown } from 'floating-vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { 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'
|
||||
import {
|
||||
appUpdateState,
|
||||
downloadAvailableAppUpdate,
|
||||
installAvailableAppUpdate,
|
||||
} from '@/providers/app-update'
|
||||
|
||||
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',
|
||||
},
|
||||
update: {
|
||||
id: 'app.action-bar.update',
|
||||
defaultMessage: 'Update',
|
||||
},
|
||||
downloadingUpdate: {
|
||||
id: 'app.action-bar.downloading-update',
|
||||
defaultMessage: 'Downloading update',
|
||||
},
|
||||
reloadToUpdate: {
|
||||
id: 'app.action-bar.reload-to-update',
|
||||
defaultMessage: 'Reload to update',
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
downloading,
|
||||
downloadPercent,
|
||||
downloadProgress,
|
||||
finishedDownloading,
|
||||
isVisible: isUpdateVisible,
|
||||
metered,
|
||||
restarting,
|
||||
} = appUpdateState
|
||||
|
||||
const isUpdateDownloading = computed(
|
||||
() =>
|
||||
downloading.value ||
|
||||
(downloadProgress.value > 0 && downloadProgress.value < 1 && !finishedDownloading.value),
|
||||
)
|
||||
const showUpdatePill = computed(
|
||||
() => isUpdateVisible.value && (finishedDownloading.value || metered.value),
|
||||
)
|
||||
const animateReadyPill = ref(false)
|
||||
const updateLabel = computed(() => {
|
||||
if (isUpdateDownloading.value) {
|
||||
return formatMessage(messages.downloadingUpdate)
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
return formatMessage(messages.reloadToUpdate)
|
||||
}
|
||||
|
||||
return formatMessage(messages.update)
|
||||
})
|
||||
const updatePillWidthClass = computed(() => {
|
||||
if (isUpdateDownloading.value) {
|
||||
return 'w-[219px]'
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
return 'w-[166px]'
|
||||
}
|
||||
|
||||
return '!w-[96px]'
|
||||
})
|
||||
let readyPillAnimationFrame: number | null = null
|
||||
watch([showUpdatePill, finishedDownloading], async ([show, ready], [wasShown, wasReady]) => {
|
||||
if (readyPillAnimationFrame !== null) {
|
||||
cancelAnimationFrame(readyPillAnimationFrame)
|
||||
readyPillAnimationFrame = null
|
||||
}
|
||||
|
||||
if (!show || !ready) {
|
||||
animateReadyPill.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (wasShown && wasReady) {
|
||||
return
|
||||
}
|
||||
|
||||
animateReadyPill.value = false
|
||||
await nextTick()
|
||||
readyPillAnimationFrame = requestAnimationFrame(() => {
|
||||
animateReadyPill.value = true
|
||||
readyPillAnimationFrame = null
|
||||
})
|
||||
})
|
||||
async function handleUpdateClick() {
|
||||
if (isUpdateDownloading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (finishedDownloading.value) {
|
||||
await installAvailableAppUpdate()
|
||||
} else {
|
||||
await downloadAvailableAppUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
const currentProcesses = ref<RunningProcess[]>([])
|
||||
const selectedProcess = ref<RunningProcess | undefined>()
|
||||
|
||||
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 currentLoadingBarIconUrls = ref<Record<string, string | null>>({})
|
||||
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 getDisplayIconUrl(icon: string | null | undefined): string | null {
|
||||
if (!icon) {
|
||||
return null
|
||||
}
|
||||
if (/^(https?:|data:|blob:|asset:|tauri:)/.test(icon)) {
|
||||
return icon
|
||||
}
|
||||
return convertFileSrc(icon)
|
||||
}
|
||||
|
||||
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),
|
||||
iconUrl: currentLoadingBarIconUrls.value[getLoadingBarKey(bar)] ?? null,
|
||||
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')
|
||||
|
||||
const profilePaths = Array.from(
|
||||
new Set(
|
||||
currentLoadingBars.value
|
||||
.map((bar) => bar.bar_type?.profile_path)
|
||||
.filter((path): path is string => !!path),
|
||||
),
|
||||
)
|
||||
const profiles = profilePaths.length
|
||||
? await getInstances(profilePaths).catch((error) => {
|
||||
handleError(error)
|
||||
return []
|
||||
})
|
||||
: []
|
||||
const profileIconUrls = new Map(
|
||||
profiles.map((profile) => [profile.path, getDisplayIconUrl(profile.icon_path)]),
|
||||
)
|
||||
currentLoadingBarIconUrls.value = Object.fromEntries(
|
||||
currentLoadingBars.value.map((bar) => {
|
||||
const barIconUrl = getDisplayIconUrl(bar.bar_type?.icon)
|
||||
const profileIconUrl = bar.bar_type?.profile_path
|
||||
? profileIconUrls.get(bar.bar_type.profile_path)
|
||||
: null
|
||||
return [getLoadingBarKey(bar), barIconUrl ?? profileIconUrl ?? null]
|
||||
}),
|
||||
)
|
||||
|
||||
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()
|
||||
if (readyPillAnimationFrame !== null) {
|
||||
cancelAnimationFrame(readyPillAnimationFrame)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.update-pill-ready-hidden {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.update-pill-ready-visible {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
</style>
|
||||
@@ -61,7 +61,7 @@ defineExpose({
|
||||
errorType.value = 'directory_move'
|
||||
supportLink.value = 'https://support.modrinth.com'
|
||||
|
||||
if (errorVal.message.includes('directory is not writeable')) {
|
||||
if (errorVal.message.includes('directory is not writable')) {
|
||||
metadata.value.readOnly = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { WrenchIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||
@@ -40,9 +41,13 @@ const messages = defineMessages({
|
||||
},
|
||||
selectFilesLabel: {
|
||||
id: 'app.export-modal.select-files-label',
|
||||
defaultMessage: 'Select files and folders to include in pack',
|
||||
defaultMessage: 'Configure which files are included in this export',
|
||||
},
|
||||
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||
includeFile: {
|
||||
id: 'app.export-modal.include-file-accessibility-label',
|
||||
defaultMessage: 'Include "{file}"?',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -65,7 +70,6 @@ const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const folders = ref([])
|
||||
const showingFiles = ref(false)
|
||||
|
||||
const initFiles = async () => {
|
||||
const newFolders = new Map()
|
||||
@@ -87,7 +91,12 @@ const initFiles = async () => {
|
||||
folder.startsWith('modrinth_logs') ||
|
||||
folder.startsWith('.fabric'),
|
||||
}))
|
||||
.filter((pathData) => !pathData.path.includes('.DS_Store'))
|
||||
.filter(
|
||||
(pathData) =>
|
||||
!pathData.path.includes('.DS_Store') &&
|
||||
pathData.path !== 'mods/.connector' &&
|
||||
!pathData.path.startsWith('mods/.connector/'),
|
||||
)
|
||||
.forEach((pathData) => {
|
||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||
if (parent !== '') {
|
||||
@@ -121,15 +130,20 @@ const exportPack = async () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
const outputPath = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
const outputPath = await save({
|
||||
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
filters: [
|
||||
{
|
||||
name: 'Modrinth Modpack',
|
||||
extensions: ['mrpack'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (outputPath) {
|
||||
export_profile_mrpack(
|
||||
props.instance.path,
|
||||
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
outputPath,
|
||||
filesToExport,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
@@ -142,188 +156,104 @@ const exportPack = async () => {
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
|
||||
<div class="modal-body">
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<div class="flex flex-col gap-4 w-[40rem]">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||
|
||||
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table">
|
||||
<div class="table-head">
|
||||
<div class="table-cell row-wise">
|
||||
{{ formatMessage(messages.selectFilesLabel) }}
|
||||
<Button
|
||||
class="sleek-primary collapsed-button"
|
||||
icon-only
|
||||
@click="() => (showingFiles = !showingFiles)"
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
<Accordion
|
||||
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
|
||||
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
|
||||
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
Configure which files are included in this export
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
|
||||
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
|
||||
<Accordion
|
||||
class="flex flex-col"
|
||||
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
|
||||
>
|
||||
<PlusIcon v-if="!showingFiles" />
|
||||
<XIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showingFiles" class="table-content">
|
||||
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
|
||||
<div class="table-cell file-entry">
|
||||
<div class="file-primary">
|
||||
<template #title>
|
||||
<Checkbox
|
||||
:model-value="children.every((child) => child.selected)"
|
||||
:label="path.name"
|
||||
class="select-checkbox"
|
||||
:indeterminate="
|
||||
!children.every((child) => child.selected) &&
|
||||
children.some((child) => child.selected)
|
||||
"
|
||||
:description="formatMessage(messages.includeFile, { file: path.name })"
|
||||
class="pl-4 py-2"
|
||||
:disabled="children.every((x) => x.disabled)"
|
||||
@update:model-value="
|
||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||
"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
|
||||
</template>
|
||||
<div v-for="child in children" :key="child.path">
|
||||
<Checkbox
|
||||
v-model="path.showingMore"
|
||||
class="select-checkbox dropdown"
|
||||
collapsing-toggle-style
|
||||
v-model="child.selected"
|
||||
:label="child.name"
|
||||
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
|
||||
:disabled="child.disabled"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="path.showingMore" class="file-secondary">
|
||||
<div v-for="child in children" :key="child.path" class="file-secondary-row">
|
||||
<Checkbox
|
||||
v-model="child.selected"
|
||||
:label="child.name"
|
||||
class="select-checkbox"
|
||||
:disabled="child.disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="file in files" :key="file.path" class="table-row">
|
||||
<div class="table-cell file-entry">
|
||||
<div class="file-primary">
|
||||
<Checkbox
|
||||
v-model="file.selected"
|
||||
:label="file.name"
|
||||
:disabled="file.disabled"
|
||||
class="select-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
<Checkbox
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
v-model="file.selected"
|
||||
:label="file.name"
|
||||
:disabled="file.disabled"
|
||||
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row push-right">
|
||||
<Button @click="exportModal.hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button color="primary" @click="exportPack">
|
||||
<PackageIcon />
|
||||
{{ formatMessage(messages.exportButton) }}
|
||||
</Button>
|
||||
</Accordion>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="exportModal.hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="exportPack">
|
||||
<PackageIcon />
|
||||
{{ formatMessage(messages.exportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
|
||||
.labeled_input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.select-checkbox {
|
||||
gap: var(--gap-sm);
|
||||
|
||||
button.checkbox {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.table-content {
|
||||
max-height: 18rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 1px solid var(--color-bg);
|
||||
}
|
||||
|
||||
.file-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.file-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.file-secondary {
|
||||
margin-left: var(--gap-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
height: 100%;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.file-secondary-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-wise {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
<template>
|
||||
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
||||
<div class="auto-detect-modal">
|
||||
<div class="table">
|
||||
<div class="table-row table-head">
|
||||
<div class="table-cell table-text">Version</div>
|
||||
<div class="table-cell table-text">Path</div>
|
||||
<div class="table-cell table-text">Actions</div>
|
||||
</div>
|
||||
<div v-for="javaInstall in chosenInstallOptions" :key="javaInstall.path" class="table-row">
|
||||
<div class="table-cell table-text">
|
||||
<span>{{ javaInstall.version }}</span>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
|
||||
<template #cell-version="{ value }">
|
||||
<span class="font-semibold text-primary">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-path="{ value }">
|
||||
<span v-tooltip="value" class="block truncate font-mono text-xs">{{ value }}</span>
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex items-center justify-end">
|
||||
<ButtonStyled v-if="currentSelected.path === row.path">
|
||||
<button class="!shadow-none" disabled><CheckIcon /> Selected</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-tooltip="javaInstall.path" class="table-cell table-text">
|
||||
<span>{{ javaInstall.path }}</span>
|
||||
</div>
|
||||
<div class="table-cell table-text manage">
|
||||
<Button v-if="currentSelected.path === javaInstall.path" disabled
|
||||
><CheckIcon /> Selected</Button
|
||||
>
|
||||
<Button v-else @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
|
||||
<div class="table-cell table-text">No java installations found!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<Button @click="$refs.detectJavaModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
</template>
|
||||
<template #empty-state>
|
||||
<div class="p-4 text-secondary">No java installations found!</div>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="flex justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="!shadow-none !border-surface-4 !border"
|
||||
@click="$refs.detectJavaModal.hide()"
|
||||
>
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
<script setup>
|
||||
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -48,6 +50,11 @@ const { handleError } = injectNotificationManager()
|
||||
const chosenInstallOptions = ref([])
|
||||
const detectJavaModal = ref(null)
|
||||
const currentSelected = ref({})
|
||||
const javaInstallColumns = [
|
||||
{ key: 'version', label: 'Version', width: '9rem' },
|
||||
{ key: 'path', label: 'Path' },
|
||||
{ key: 'actions', label: 'Actions', align: 'right', width: '10rem' },
|
||||
]
|
||||
|
||||
defineExpose({
|
||||
show: async (version, currentSelectedJava) => {
|
||||
@@ -73,25 +80,3 @@ function setJavaInstall(javaInstall) {
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.auto-detect-modal {
|
||||
.table {
|
||||
.table-row {
|
||||
grid-template-columns: 1fr 4fr min-content;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inherit;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.manage {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,75 +1,104 @@
|
||||
<template>
|
||||
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
|
||||
<div class="toggle-setting" :class="{ compact }">
|
||||
<StyledInput
|
||||
autocomplete="off"
|
||||
:disabled="props.disabled"
|
||||
:model-value="props.modelValue ? props.modelValue.path : ''"
|
||||
:placeholder="placeholder ?? '/path/to/java'"
|
||||
wrapper-class="installation-input"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
path: val,
|
||||
})
|
||||
}
|
||||
"
|
||||
/>
|
||||
<span class="installation-buttons">
|
||||
<Button
|
||||
v-if="props.version"
|
||||
:disabled="props.disabled || installingJava"
|
||||
@click="reinstallJava"
|
||||
<div :id="props.id" class="toggle-setting" :class="{ compact }">
|
||||
<div class="input-with-status">
|
||||
<StyledInput
|
||||
autocomplete="off"
|
||||
:disabled="props.disabled"
|
||||
:model-value="props.modelValue ? props.modelValue.path : ''"
|
||||
:placeholder="placeholder ?? '/path/to/java'"
|
||||
wrapper-class="installation-input"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
path: val,
|
||||
})
|
||||
}
|
||||
"
|
||||
/>
|
||||
<ButtonStyled
|
||||
:color="
|
||||
!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
"
|
||||
color-fill="text"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</Button>
|
||||
<Button v-if="testingJava" disabled> Testing... </Button>
|
||||
<Button v-else-if="testingJavaSuccess === true">
|
||||
<CheckIcon class="test-success" />
|
||||
Success
|
||||
</Button>
|
||||
<Button v-else-if="testingJavaSuccess === false">
|
||||
<XIcon class="test-fail" />
|
||||
Failed
|
||||
</Button>
|
||||
<Button v-else :disabled="props.disabled" @click="testJava">
|
||||
<PlayIcon />
|
||||
Test
|
||||
</Button>
|
||||
<button
|
||||
class="!shadow-none"
|
||||
:disabled="testingJava || props.disabled"
|
||||
@click="runTest(props.modelValue?.path)"
|
||||
@mouseenter="!props.disabled && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="testingJavaSuccess === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<span class="installation-buttons">
|
||||
<ButtonStyled v-if="props.version">
|
||||
<button
|
||||
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
|
||||
class="!shadow-none"
|
||||
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
|
||||
@click="reinstallJava"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
CheckIcon,
|
||||
CheckCircleIcon,
|
||||
DownloadIcon,
|
||||
FolderSearchIcon,
|
||||
PlayIcon,
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
SpinnerIcon,
|
||||
XCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||
import useJavaTest from '@/composables/useJavaTest'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
||||
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
required: false,
|
||||
@@ -100,29 +129,36 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const testingJava = ref(false)
|
||||
const testingJavaSuccess = ref(null)
|
||||
const {
|
||||
testingJava,
|
||||
javaTestResult: testingJavaSuccess,
|
||||
testJavaInstallationDebounced,
|
||||
testJavaInstallation,
|
||||
} = useJavaTest()
|
||||
|
||||
const installingJava = ref(false)
|
||||
const hoveringTest = ref(false)
|
||||
let hasInitialized = false
|
||||
|
||||
async function testJava() {
|
||||
testingJava.value = true
|
||||
testingJavaSuccess.value = await test_jre(
|
||||
props.modelValue ? props.modelValue.path : '',
|
||||
props.version,
|
||||
)
|
||||
testingJava.value = false
|
||||
|
||||
trackEvent('JavaTest', {
|
||||
path: props.modelValue ? props.modelValue.path : '',
|
||||
success: testingJavaSuccess.value,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
testingJavaSuccess.value = null
|
||||
}, 2000)
|
||||
async function runTest(path) {
|
||||
await testJavaInstallation(path, props.version, true)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue?.path,
|
||||
(newPath) => {
|
||||
if (newPath) {
|
||||
if (!hasInitialized) {
|
||||
testJavaInstallation(newPath, props.version, false)
|
||||
hasInitialized = true
|
||||
} else {
|
||||
testJavaInstallationDebounced(newPath, props.version)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function handleJavaFileInput() {
|
||||
const filePath = await open()
|
||||
|
||||
@@ -132,6 +168,7 @@ async function handleJavaFileInput() {
|
||||
result = {
|
||||
path: filePath.path ?? filePath,
|
||||
version: props.version.toString(),
|
||||
parsed_version: props.version,
|
||||
architecture: 'x86',
|
||||
}
|
||||
}
|
||||
@@ -165,6 +202,7 @@ async function reinstallJava() {
|
||||
result = {
|
||||
path: path,
|
||||
version: props.version.toString(),
|
||||
parsed_version: props.version,
|
||||
architecture: 'x86',
|
||||
}
|
||||
}
|
||||
@@ -176,13 +214,23 @@ async function reinstallJava() {
|
||||
|
||||
emit('update:modelValue', result)
|
||||
installingJava.value = false
|
||||
runTest(result.path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-with-status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.installation-input {
|
||||
width: 100% !important;
|
||||
flex-grow: 1;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toggle-setting {
|
||||
@@ -204,17 +252,5 @@ async function reinstallJava() {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
|
||||
.btn {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.test-success {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.test-fail {
|
||||
color: var(--color-red);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { CheckIcon } from '@modrinth/assets'
|
||||
import { Badge, Button } from '@modrinth/ui'
|
||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
@@ -74,15 +74,18 @@ const onHide = () => {
|
||||
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<Button
|
||||
:color="version.id === installedVersion ? '' : 'primary'"
|
||||
icon-only
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
<ButtonStyled
|
||||
circular
|
||||
:color="version.id === installedVersion ? 'standard' : 'brand'"
|
||||
>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</Button>
|
||||
<button
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="name-cell table-cell table-text">
|
||||
<div class="version-link">
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { CalendarIcon, UsersIcon, XIcon } from '@modrinth/assets'
|
||||
import { injectModrinthClient, ProgressBar } from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const DISMISSED_STORAGE_KEY = 'pride-fundraiser-2026-dismissed'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
const dismissed = ref(localStorage.getItem(DISMISSED_STORAGE_KEY) === 'true')
|
||||
|
||||
const { data: campaignInfo } = useQuery({
|
||||
queryKey: ['campaign', 'pride-26'],
|
||||
queryFn: () => client.labrinth.campaign_internal.getPride26(),
|
||||
enabled: () => !dismissed.value,
|
||||
staleTime: 15 * 60 * 1000,
|
||||
refetchInterval: 15 * 60 * 1000,
|
||||
retry: false,
|
||||
})
|
||||
const shouldShowBanner = computed(
|
||||
() => !dismissed.value && Number(campaignInfo.value?.target_usd) > 0,
|
||||
)
|
||||
|
||||
async function openPrideFundraiser() {
|
||||
await openUrl('https://modrinth.com/pride?from=app')
|
||||
}
|
||||
|
||||
function dismissBanner() {
|
||||
dismissed.value = true
|
||||
localStorage.setItem(DISMISSED_STORAGE_KEY, 'true')
|
||||
}
|
||||
|
||||
function formatUsd(amount: string | number) {
|
||||
return Number(amount).toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
}
|
||||
|
||||
function daysLeft() {
|
||||
return Math.max(
|
||||
0,
|
||||
Math.ceil((new Date('2026-07-01T00:00:00Z').getTime() - Date.now()) / (24 * 60 * 60 * 1000)),
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="shouldShowBanner && campaignInfo">
|
||||
<section
|
||||
role="link"
|
||||
tabindex="0"
|
||||
class="flex w-full cursor-pointer flex-col gap-3 rounded-xl border border-solid border-surface-5 bg-button-bg p-3 text-primary transition-[border-color,filter] hover:border-surface-6 hover:brightness-125 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
||||
aria-label="Open Pride fundraiser"
|
||||
@click="openPrideFundraiser"
|
||||
@keydown.enter="openPrideFundraiser"
|
||||
@keydown.space.prevent="openPrideFundraiser"
|
||||
>
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<h2 class="m-0 min-w-0 truncate text-base font-semibold text-contrast">
|
||||
Pride Fundraiser 2026
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="m-0 flex size-5 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-primary transition-colors hover:text-contrast focus-visible:text-contrast"
|
||||
aria-label="Dismiss Pride fundraiser"
|
||||
@click.stop="dismissBanner"
|
||||
@keydown.stop
|
||||
>
|
||||
<XIcon aria-hidden="true" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-px w-full bg-surface-5" />
|
||||
<div class="flex w-full flex-col gap-2.5">
|
||||
<div class="flex items-end gap-1 whitespace-nowrap">
|
||||
<span class="text-base font-semibold leading-5 text-contrast">
|
||||
{{ formatUsd(campaignInfo.total_donations_usd) }}
|
||||
</span>
|
||||
<span class="text-xs font-medium leading-4">
|
||||
of {{ formatUsd(campaignInfo.target_usd) }}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
class="pride-fundraiser-banner__progress"
|
||||
:progress="Number(campaignInfo.total_donations_usd)"
|
||||
:max="Number(campaignInfo.target_usd)"
|
||||
color="purple"
|
||||
full-width
|
||||
:gradient-border="false"
|
||||
:aria-label="`${formatUsd(campaignInfo.total_donations_usd)} of ${formatUsd(
|
||||
campaignInfo.target_usd,
|
||||
)} raised`"
|
||||
/>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs font-medium leading-4">
|
||||
<span class="flex items-center gap-1">
|
||||
<UsersIcon aria-hidden="true" class="size-4 shrink-0" />
|
||||
{{ campaignInfo.num_donators.toLocaleString('en-US') }}
|
||||
{{ campaignInfo.num_donators === 1 ? 'supporter' : 'supporters' }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<CalendarIcon aria-hidden="true" class="size-4 shrink-0" />
|
||||
{{ daysLeft() }} {{ daysLeft() === 1 ? 'day left' : 'days left' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pride-fundraiser-banner__progress :deep(.progress-bar) {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-red) 0%,
|
||||
var(--color-orange) 20%,
|
||||
var(--color-green) 50%,
|
||||
var(--color-blue) 75%,
|
||||
var(--color-purple) 100%
|
||||
);
|
||||
}
|
||||
</style>
|
||||
@@ -37,6 +37,6 @@ defineProps({
|
||||
|
||||
.progress-bar__fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s;
|
||||
transition: width 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Button, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -12,7 +12,6 @@ const { install: installVersion } = injectContentInstall()
|
||||
const confirmModal = ref(null)
|
||||
const project = ref(null)
|
||||
const version = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
defineExpose({
|
||||
async show(event) {
|
||||
@@ -70,7 +69,9 @@ async function install() {
|
||||
</p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button :loading="installing" color="primary" @click="install">Install</Button>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="install">Install</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<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 close-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: -9px -6px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.expanded-button.close-button::before {
|
||||
inset: -9px -9px -9px -6px;
|
||||
}
|
||||
</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 }">
|
||||
@@ -393,7 +393,6 @@ const messages = defineMessages({
|
||||
<FriendsSection
|
||||
v-if="pendingFriends.length > 0"
|
||||
:is-searching="!!search"
|
||||
open-by-default
|
||||
:friends="pendingFriends"
|
||||
:heading="formatMessage(messages.pending)"
|
||||
:remove-friend="removeFriend"
|
||||
|
||||
@@ -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))
|
||||
"
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<ModalWrapper ref="incompatibleModal" header="Incompatibility warning" :on-hide="onInstall">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
This {{ versions?.length > 0 ? 'project' : 'version' }} is not compatible with the instance
|
||||
you're trying to install it on. Are you sure you want to continue? Dependencies will not be
|
||||
installed.
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th>{{ instance?.name }}</th>
|
||||
<th>{{ project.title }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="content">
|
||||
<td class="data">{{ instance?.loader }} {{ instance?.game_version }}</td>
|
||||
<td>
|
||||
<Combobox
|
||||
v-if="versions?.length > 1"
|
||||
v-model="selectedVersionId"
|
||||
:options="versionOptions"
|
||||
:searchable="true"
|
||||
placeholder="Select version"
|
||||
force-direction="up"
|
||||
:max-height="150"
|
||||
/>
|
||||
<span v-else>
|
||||
<span>{{ selectedVersionLabel }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="button-group">
|
||||
<Button @click="() => incompatibleModal.hide()"><XIcon />Cancel</Button>
|
||||
<Button color="primary" :disabled="installing" @click="install()">
|
||||
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, Combobox, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const instance = ref(null)
|
||||
const project = ref(null)
|
||||
const versions = ref(null)
|
||||
const selectedVersion = ref(null)
|
||||
const incompatibleModal = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
const selectedVersionLabel = computed(() => {
|
||||
if (!selectedVersion.value) return ''
|
||||
return `${selectedVersion.value.name} (${selectedVersion.value.loaders
|
||||
.map((name) => formatLoader(formatMessage, name))
|
||||
.join(', ')} - ${selectedVersion.value.game_versions.join(', ')})`
|
||||
})
|
||||
|
||||
const versionOptions = computed(() =>
|
||||
(versions.value ?? []).map((version) => ({
|
||||
value: version.id,
|
||||
label: `${version.name} (${version.loaders
|
||||
.map((name) => formatLoader(formatMessage, name))
|
||||
.join(', ')} - ${version.game_versions.join(', ')})`,
|
||||
})),
|
||||
)
|
||||
|
||||
const selectedVersionId = computed({
|
||||
get: () => selectedVersion.value?.id ?? null,
|
||||
set: (value) => {
|
||||
if (!value) return
|
||||
selectedVersion.value = (versions.value ?? []).find((version) => version.id === value) ?? null
|
||||
},
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
|
||||
instance.value = instanceVal
|
||||
versions.value = projectVersions ?? []
|
||||
selectedVersion.value = selected ?? projectVersions?.[0] ?? null
|
||||
|
||||
project.value = projectVal
|
||||
|
||||
onInstall.value = callback
|
||||
installing.value = false
|
||||
|
||||
incompatibleModal.value.show()
|
||||
|
||||
trackEvent('ProjectInstallStart', { source: 'ProjectIncompatibilityWarningModal' })
|
||||
},
|
||||
})
|
||||
|
||||
const install = async () => {
|
||||
installing.value = true
|
||||
await installMod(instance.value.path, selectedVersion.value.id).catch(handleError)
|
||||
installing.value = false
|
||||
onInstall.value(selectedVersion.value.id)
|
||||
incompatibleModal.value.hide()
|
||||
|
||||
trackEvent('ProjectInstall', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
id: project.value,
|
||||
version_id: selectedVersion.value.id,
|
||||
project_type: project.value.project_type,
|
||||
title: project.value.title,
|
||||
source: 'ProjectIncompatibilityWarningModal',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.data {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
border-collapse: collapse;
|
||||
box-shadow: 0 0 0 1px var(--color-button-bg);
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-bg);
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--color-button-bg);
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
border-top-left-radius: var(--radius-lg);
|
||||
border-right: 1px solid var(--color-button-bg);
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
border-top-right-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
border-right: 1px solid var(--color-button-bg);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,418 +0,0 @@
|
||||
<script setup>
|
||||
import {
|
||||
CheckIcon,
|
||||
DownloadIcon,
|
||||
PlusIcon,
|
||||
RightArrowIcon,
|
||||
SearchIcon,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, Card, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_v3_many } from '@/helpers/cache.js'
|
||||
import {
|
||||
add_project_from_version as installMod,
|
||||
check_installed,
|
||||
create,
|
||||
get,
|
||||
list,
|
||||
} from '@/helpers/profile'
|
||||
import {
|
||||
findPreferredVersion,
|
||||
installVersionDependencies,
|
||||
isVersionCompatible,
|
||||
} from '@/store/install.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const router = useRouter()
|
||||
|
||||
const versions = ref()
|
||||
const project = ref()
|
||||
|
||||
const installModal = ref()
|
||||
const searchFilter = ref('')
|
||||
|
||||
const showCreation = ref(false)
|
||||
const icon = ref(null)
|
||||
const name = ref(null)
|
||||
const display_icon = ref(null)
|
||||
const loader = ref(null)
|
||||
const gameVersion = ref(null)
|
||||
const creatingInstance = ref(false)
|
||||
|
||||
const profiles = ref([])
|
||||
|
||||
const shownProfiles = computed(() =>
|
||||
profiles.value.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
}),
|
||||
)
|
||||
|
||||
const isProfileCompatible = (profile) =>
|
||||
versions.value?.some((version) => isVersionCompatible(version, project.value, profile))
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: async (projectVal, versionsVal, callback) => {
|
||||
project.value = projectVal
|
||||
versions.value = versionsVal
|
||||
searchFilter.value = ''
|
||||
|
||||
showCreation.value = false
|
||||
name.value = null
|
||||
icon.value = null
|
||||
display_icon.value = null
|
||||
gameVersion.value = null
|
||||
loader.value = null
|
||||
|
||||
onInstall.value = callback
|
||||
|
||||
const profilesVal = await list().catch(handleError)
|
||||
for (const profile of profilesVal) {
|
||||
profile.installing = false
|
||||
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
||||
handleError,
|
||||
)
|
||||
}
|
||||
|
||||
const linkedProjectIds = profilesVal
|
||||
.filter((p) => p.linked_data?.project_id)
|
||||
.map((p) => p.linked_data.project_id)
|
||||
if (linkedProjectIds.length > 0) {
|
||||
const linkedProjects = await get_project_v3_many(linkedProjectIds, 'must_revalidate').catch(
|
||||
() => [],
|
||||
)
|
||||
const serverProjectIds = new Set(
|
||||
linkedProjects.filter((p) => p?.minecraft_server != null).map((p) => p.id),
|
||||
)
|
||||
for (const profile of profilesVal) {
|
||||
profile.isServerInstance = serverProjectIds.has(profile.linked_data?.project_id)
|
||||
}
|
||||
}
|
||||
|
||||
profiles.value = profilesVal
|
||||
|
||||
installModal.value.show()
|
||||
|
||||
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
||||
},
|
||||
})
|
||||
|
||||
async function install(instance) {
|
||||
instance.installing = true
|
||||
const version = findPreferredVersion(versions.value, project.value, instance)
|
||||
|
||||
if (!version) {
|
||||
instance.installing = false
|
||||
handleError('No compatible version found')
|
||||
return
|
||||
}
|
||||
|
||||
await installMod(instance.path, version.id).catch(handleError)
|
||||
await installVersionDependencies(instance, version).catch(handleError)
|
||||
|
||||
instance.installedMod = true
|
||||
instance.installing = false
|
||||
|
||||
trackEvent('ProjectInstall', {
|
||||
loader: instance.loader,
|
||||
game_version: instance.game_version,
|
||||
id: project.value.id,
|
||||
version_id: version.id,
|
||||
project_type: project.value.project_type,
|
||||
title: project.value.title,
|
||||
source: 'ProjectInstallModal',
|
||||
})
|
||||
|
||||
onInstall.value(version.id)
|
||||
}
|
||||
|
||||
const toggleCreation = () => {
|
||||
showCreation.value = !showCreation.value
|
||||
name.value = null
|
||||
icon.value = null
|
||||
display_icon.value = null
|
||||
gameVersion.value = null
|
||||
loader.value = null
|
||||
|
||||
if (showCreation.value) {
|
||||
trackEvent('InstanceCreateStart', { source: 'ProjectInstallModal' })
|
||||
}
|
||||
}
|
||||
|
||||
const upload_icon = async () => {
|
||||
const res = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: 'Image',
|
||||
extensions: ['png', 'jpeg'],
|
||||
},
|
||||
],
|
||||
})
|
||||
icon.value = res.path ?? res
|
||||
|
||||
if (!icon.value) return
|
||||
display_icon.value = convertFileSrc(icon.value)
|
||||
}
|
||||
|
||||
const reset_icon = () => {
|
||||
icon.value = null
|
||||
display_icon.value = null
|
||||
}
|
||||
|
||||
const createInstance = async () => {
|
||||
creatingInstance.value = true
|
||||
|
||||
const gameVersions = versions.value[0].game_versions
|
||||
const gameVersion = gameVersions[0]
|
||||
|
||||
const loaders = versions.value[0].loaders
|
||||
const loader = loaders.includes('fabric')
|
||||
? 'fabric'
|
||||
: loaders.includes('neoforge')
|
||||
? 'neoforge'
|
||||
: loaders.includes('forge')
|
||||
? 'forge'
|
||||
: loaders.includes('quilt')
|
||||
? 'quilt'
|
||||
: 'vanilla'
|
||||
|
||||
const id = await create(name.value, gameVersion, loader, 'latest', icon.value).catch(handleError)
|
||||
|
||||
await installMod(id, versions.value[0].id).catch(handleError)
|
||||
|
||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||
|
||||
const instance = await get(id, true)
|
||||
await installVersionDependencies(instance, versions.value[0]).catch(handleError)
|
||||
|
||||
trackEvent('InstanceCreate', {
|
||||
profile_name: name.value,
|
||||
game_version: versions.value[0].game_versions[0],
|
||||
loader: loader,
|
||||
loader_version: 'latest',
|
||||
has_icon: !!icon.value,
|
||||
source: 'ProjectInstallModal',
|
||||
})
|
||||
|
||||
trackEvent('ProjectInstall', {
|
||||
loader: loader,
|
||||
game_version: versions.value[0].game_versions[0],
|
||||
id: project.value,
|
||||
version_id: versions.value[0].id,
|
||||
project_type: project.value.project_type,
|
||||
title: project.value.title,
|
||||
source: 'ProjectInstallModal',
|
||||
})
|
||||
|
||||
onInstall.value(versions.value[0].id)
|
||||
|
||||
if (installModal.value) installModal.value.hide()
|
||||
creatingInstance.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="installModal" header="Install project to instance" :on-hide="onInstall">
|
||||
<div class="modal-body">
|
||||
<StyledInput
|
||||
v-model="searchFilter"
|
||||
:icon="SearchIcon"
|
||||
type="search"
|
||||
placeholder="Search for an instance"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<div class="profiles" :class="{ 'hide-creation': !showCreation }">
|
||||
<div v-for="profile in shownProfiles" :key="profile.name" class="option">
|
||||
<router-link
|
||||
class="btn btn-transparent profile-button"
|
||||
:to="`/instance/${encodeURIComponent(profile.path)}`"
|
||||
@click="installModal.hide()"
|
||||
>
|
||||
<Avatar
|
||||
:src="profile.icon_path ? convertFileSrc(profile.icon_path) : null"
|
||||
class="profile-image"
|
||||
/>
|
||||
{{ profile.name }}
|
||||
</router-link>
|
||||
<div
|
||||
v-tooltip="
|
||||
profile.linked_data?.locked && !profile.installedMod
|
||||
? 'Unpair or unlock an instance to add mods.'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<Button
|
||||
:disabled="
|
||||
!isProfileCompatible(profile) || profile.installedMod || profile.installing
|
||||
"
|
||||
@click="install(profile)"
|
||||
>
|
||||
<DownloadIcon
|
||||
v-if="isProfileCompatible(profile) && !profile.installedMod && !profile.installing"
|
||||
/>
|
||||
<CheckIcon v-else-if="profile.installedMod" />
|
||||
{{
|
||||
profile.installing
|
||||
? 'Installing...'
|
||||
: profile.installedMod
|
||||
? 'Installed'
|
||||
: !isProfileCompatible(profile)
|
||||
? 'Incompatible'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Card v-if="showCreation" class="creation-card">
|
||||
<div class="creation-container">
|
||||
<div class="creation-icon">
|
||||
<Avatar size="md" class="icon" :src="display_icon" />
|
||||
<div class="creation-icon__description">
|
||||
<Button @click="upload_icon()">
|
||||
<UploadIcon />
|
||||
<span class="no-wrap"> Select icon </span>
|
||||
</Button>
|
||||
<Button :disabled="!display_icon" @click="reset_icon()">
|
||||
<XIcon />
|
||||
<span class="no-wrap"> Remove icon </span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="creation-settings">
|
||||
<StyledInput
|
||||
v-model="name"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
class="creation-input"
|
||||
/>
|
||||
<Button :disabled="creatingInstance === true || !name" @click="createInstance()">
|
||||
<RightArrowIcon />
|
||||
{{ creatingInstance ? 'Creating...' : 'Create' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="input-group push-right">
|
||||
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
|
||||
<PlusIcon />
|
||||
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
|
||||
</Button>
|
||||
<Button @click="installModal.hide()">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.creation-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.creation-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.creation-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
.creation-icon__description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.creation-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.creation-dropdown {
|
||||
width: min-content !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.creation-settings {
|
||||
width: 100%;
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.profiles {
|
||||
max-height: 12rem;
|
||||
overflow-y: auto;
|
||||
|
||||
&.hide-creation {
|
||||
max-height: 21rem;
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
width: calc(100%);
|
||||
background: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-button {
|
||||
align-content: start;
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-image {
|
||||
--size: 2rem !important;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -4,12 +4,14 @@ import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
Chips,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { computed, type Ref, ref, watch } from 'vue'
|
||||
@@ -25,14 +27,22 @@ import type { GameInstance } from '../../../helpers/types'
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const deleteConfirmModal = ref()
|
||||
|
||||
const { instance } = injectInstanceSettings()
|
||||
type ReleaseChannel = GameInstance['preferred_update_channel']
|
||||
const releaseChannelOptions: ReleaseChannel[] = ['release', 'beta', 'alpha']
|
||||
|
||||
const title = ref(instance.value.name)
|
||||
const icon: Ref<string | undefined> = ref(instance.value.icon_path)
|
||||
const groups = ref([...instance.value.groups])
|
||||
const savingReleaseChannel = ref(false)
|
||||
const selectedReleaseChannel = ref<ReleaseChannel>(instance.value.preferred_update_channel)
|
||||
const releaseChannelDisabledItems = computed<ReleaseChannel[]>(() =>
|
||||
savingReleaseChannel.value ? [...releaseChannelOptions] : [],
|
||||
)
|
||||
|
||||
const newCategoryInput = ref('')
|
||||
|
||||
@@ -51,6 +61,52 @@ const availableGroups = computed(() => [
|
||||
...new Set([...allInstances.value.flatMap((instance) => instance.groups), ...groups.value]),
|
||||
])
|
||||
|
||||
function formatReleaseChannelLabel(channel: ReleaseChannel) {
|
||||
switch (channel) {
|
||||
case 'release':
|
||||
return formatMessage(messages.updateChannelRelease)
|
||||
case 'beta':
|
||||
return formatMessage(messages.updateChannelBeta)
|
||||
case 'alpha':
|
||||
return formatMessage(messages.updateChannelAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
function formatReleaseChannelDescription(channel: ReleaseChannel) {
|
||||
switch (channel) {
|
||||
case 'release':
|
||||
return formatMessage(messages.updateChannelReleaseDescription)
|
||||
case 'beta':
|
||||
return formatMessage(messages.updateChannelBetaDescription)
|
||||
case 'alpha':
|
||||
return formatMessage(messages.updateChannelAlphaDescription)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [instance.value.path, instance.value.preferred_update_channel] as const,
|
||||
() => {
|
||||
if (!savingReleaseChannel.value) {
|
||||
selectedReleaseChannel.value = instance.value.preferred_update_channel
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
watch(selectedReleaseChannel, async (channel, previousChannel) => {
|
||||
const previousReleaseChannel = previousChannel ?? instance.value.preferred_update_channel
|
||||
if (channel === instance.value.preferred_update_channel) return
|
||||
|
||||
savingReleaseChannel.value = true
|
||||
const profilePath = instance.value.path
|
||||
await edit(profilePath, { preferred_update_channel: channel })
|
||||
.then(() => queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', profilePath] }))
|
||||
.catch((error) => {
|
||||
selectedReleaseChannel.value = previousReleaseChannel
|
||||
handleError(error)
|
||||
})
|
||||
savingReleaseChannel.value = false
|
||||
})
|
||||
|
||||
async function resetIcon() {
|
||||
icon.value = undefined
|
||||
await edit_icon(instance.value.path, null).catch(handleError)
|
||||
@@ -175,6 +231,38 @@ const messages = defineMessages({
|
||||
id: 'instance.settings.tabs.general.duplicate-button',
|
||||
defaultMessage: 'Duplicate',
|
||||
},
|
||||
updateChannel: {
|
||||
id: 'instance.settings.tabs.general.update-channel',
|
||||
defaultMessage: 'Update channel',
|
||||
},
|
||||
updateChannelReleaseDescription: {
|
||||
id: 'instance.settings.tabs.general.update-channel.release.description',
|
||||
defaultMessage: 'Only release versions will be shown as available updates.',
|
||||
},
|
||||
updateChannelBetaDescription: {
|
||||
id: 'instance.settings.tabs.general.update-channel.beta.description',
|
||||
defaultMessage: 'Release and beta versions will be shown as available updates.',
|
||||
},
|
||||
updateChannelAlphaDescription: {
|
||||
id: 'instance.settings.tabs.general.update-channel.alpha.description',
|
||||
defaultMessage: 'Release, beta, and alpha versions will be shown as available updates.',
|
||||
},
|
||||
updateChannelRelease: {
|
||||
id: 'instance.settings.tabs.general.update-channel.release',
|
||||
defaultMessage: 'Release',
|
||||
},
|
||||
updateChannelBeta: {
|
||||
id: 'instance.settings.tabs.general.update-channel.beta',
|
||||
defaultMessage: 'Beta',
|
||||
},
|
||||
updateChannelAlpha: {
|
||||
id: 'instance.settings.tabs.general.update-channel.alpha',
|
||||
defaultMessage: 'Alpha',
|
||||
},
|
||||
selectUpdateChannelAriaLabel: {
|
||||
id: 'instance.settings.tabs.general.update-channel.select',
|
||||
defaultMessage: 'Select update channel',
|
||||
},
|
||||
deleteInstance: {
|
||||
id: 'instance.settings.tabs.general.delete',
|
||||
defaultMessage: 'Delete instance',
|
||||
@@ -304,6 +392,23 @@ const messages = defineMessages({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5 mt-6">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.updateChannel) }}
|
||||
</h2>
|
||||
<Chips
|
||||
v-model="selectedReleaseChannel"
|
||||
:items="releaseChannelOptions"
|
||||
:format-label="formatReleaseChannelLabel"
|
||||
:capitalize="false"
|
||||
:disabled-items="releaseChannelDisabledItems"
|
||||
:aria-label="formatMessage(messages.selectUpdateChannelAriaLabel)"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatReleaseChannelDescription(selectedReleaseChannel) }}
|
||||
</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) }}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
InstallationSettingsLayout,
|
||||
provideAppBackup,
|
||||
provideInstallationSettings,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
|
||||
@@ -34,8 +35,16 @@ import type { Manifest } from '../../../helpers/types'
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const queryClient = useQueryClient()
|
||||
const debug = useDebugLogger('AppInstallationSettings')
|
||||
|
||||
const { instance, offline, isMinecraftServer, onUnlinked } = injectInstanceSettings()
|
||||
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
|
||||
|
||||
debug('metadata load: start', {
|
||||
instancePath: instance.value.path,
|
||||
loader: instance.value.loader,
|
||||
gameVersion: instance.value.game_version,
|
||||
installStage: instance.value.install_stage,
|
||||
})
|
||||
|
||||
const [
|
||||
fabric_versions,
|
||||
@@ -72,6 +81,15 @@ const [
|
||||
.catch(handleError),
|
||||
])
|
||||
|
||||
debug('metadata load: done', {
|
||||
hasFabricManifest: !!fabric_versions?.value,
|
||||
hasForgeManifest: !!forge_versions?.value,
|
||||
hasQuiltManifest: !!quilt_versions?.value,
|
||||
hasNeoforgeManifest: !!neoforge_versions?.value,
|
||||
gameVersions: all_game_versions?.value?.length ?? 0,
|
||||
availablePlatforms: loaders?.value?.map((loader) => loader.name) ?? [],
|
||||
})
|
||||
|
||||
const { data: modpackInfo } = useQuery({
|
||||
queryKey: computed(() => ['linkedModpackInfo', instance.value.path]),
|
||||
queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'),
|
||||
@@ -95,11 +113,21 @@ function getManifest(loader: string) {
|
||||
quilt: quilt_versions,
|
||||
neoforge: neoforge_versions,
|
||||
}
|
||||
return map[loader]
|
||||
const manifest = map[loader]
|
||||
debug('getManifest:', {
|
||||
loader,
|
||||
hasManifest: !!manifest?.value,
|
||||
gameVersions: manifest?.value?.gameVersions?.length ?? 0,
|
||||
})
|
||||
return manifest
|
||||
}
|
||||
|
||||
provideAppBackup({
|
||||
async createBackup() {
|
||||
debug('createBackup: start', {
|
||||
instancePath: instance.value.path,
|
||||
instanceName: instance.value.name,
|
||||
})
|
||||
const allProfiles = await list()
|
||||
const prefix = `${instance.value.name} - Backup #`
|
||||
const existingNums = allProfiles
|
||||
@@ -109,10 +137,12 @@ provideAppBackup({
|
||||
const nextNum = existingNums.length > 0 ? Math.max(...existingNums) + 1 : 1
|
||||
const newPath = await duplicate(instance.value.path)
|
||||
await edit(newPath, { name: `${prefix}${nextNum}` })
|
||||
debug('createBackup: done', { newPath, backupName: `${prefix}${nextNum}` })
|
||||
},
|
||||
})
|
||||
|
||||
provideInstallationSettings({
|
||||
closeSettings: closeModal,
|
||||
loading: ref(false),
|
||||
installationInfo: computed(() => {
|
||||
const rows = [
|
||||
@@ -164,32 +194,72 @@ provideInstallationSettings({
|
||||
const manifest = getManifest(loader)
|
||||
return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id)
|
||||
})
|
||||
return (showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')).map(
|
||||
(x) => ({ value: x.version, label: x.version }),
|
||||
)
|
||||
const result = (
|
||||
showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')
|
||||
).map((x) => ({ value: x.version, label: x.version }))
|
||||
debug('resolveGameVersions:', {
|
||||
loader,
|
||||
showSnapshots,
|
||||
totalVersions: versions.length,
|
||||
filteredVersions: filtered.length,
|
||||
resultVersions: result.length,
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
resolveLoaderVersions(loader, gameVersion) {
|
||||
if (loader === 'vanilla' || !gameVersion) return []
|
||||
const manifest = getManifest(loader)
|
||||
if (!manifest?.value) return []
|
||||
if (loader === 'fabric' || loader === 'quilt') {
|
||||
return manifest.value.gameVersions[0]?.loaders ?? []
|
||||
if (loader === 'vanilla' || !gameVersion) {
|
||||
debug('resolveLoaderVersions: skipped', { loader, gameVersion })
|
||||
return []
|
||||
}
|
||||
return manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
|
||||
const manifest = getManifest(loader)
|
||||
if (!manifest?.value) {
|
||||
debug('resolveLoaderVersions: no manifest', { loader, gameVersion })
|
||||
return []
|
||||
}
|
||||
if (loader === 'fabric' || loader === 'quilt') {
|
||||
const result = manifest.value.gameVersions[0]?.loaders ?? []
|
||||
debug('resolveLoaderVersions: fabric/quilt result', {
|
||||
loader,
|
||||
gameVersion,
|
||||
count: result.length,
|
||||
})
|
||||
return result
|
||||
}
|
||||
const result =
|
||||
manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
|
||||
debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length })
|
||||
return result
|
||||
},
|
||||
|
||||
resolveHasSnapshots(loader) {
|
||||
const versions = all_game_versions?.value ?? []
|
||||
if (loader === 'vanilla') return versions.some((x) => x.version_type !== 'release')
|
||||
if (loader === 'vanilla') {
|
||||
const result = versions.some((x) => x.version_type !== 'release')
|
||||
debug('resolveHasSnapshots: vanilla', { loader, result })
|
||||
return result
|
||||
}
|
||||
const manifest = getManifest(loader)
|
||||
const supported = versions.filter(
|
||||
(item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id),
|
||||
)
|
||||
return supported.some((x) => x.version_type !== 'release')
|
||||
const result = supported.some((x) => x.version_type !== 'release')
|
||||
debug('resolveHasSnapshots:', {
|
||||
loader,
|
||||
totalVersions: versions.length,
|
||||
supportedVersions: supported.length,
|
||||
result,
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
async save(platform, gameVersion, loaderVersionId) {
|
||||
debug('save: called', {
|
||||
instancePath: instance.value.path,
|
||||
platform,
|
||||
gameVersion,
|
||||
loaderVersionId,
|
||||
})
|
||||
const editProfile: Record<string, string | undefined> = {
|
||||
loader: platform,
|
||||
game_version: gameVersion,
|
||||
@@ -198,17 +268,21 @@ provideInstallationSettings({
|
||||
editProfile.loader_version = loaderVersionId
|
||||
}
|
||||
await edit(instance.value.path, editProfile).catch(handleError)
|
||||
debug('save: edit complete', { editProfile })
|
||||
},
|
||||
|
||||
afterSave: async () => {
|
||||
debug('afterSave: installing', { instancePath: instance.value.path })
|
||||
await install(instance.value.path, false).catch(handleError)
|
||||
trackEvent('InstanceRepair', {
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
})
|
||||
debug('afterSave: done')
|
||||
},
|
||||
|
||||
async repair() {
|
||||
debug('repair: called', { instancePath: instance.value.path })
|
||||
repairing.value = true
|
||||
await install(instance.value.path, true).catch(handleError)
|
||||
repairing.value = false
|
||||
@@ -216,9 +290,11 @@ provideInstallationSettings({
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
})
|
||||
debug('repair: done')
|
||||
},
|
||||
|
||||
async reinstallModpack() {
|
||||
debug('reinstallModpack: called', { instancePath: instance.value.path })
|
||||
reinstalling.value = true
|
||||
await update_repair_modrinth(instance.value.path).catch(handleError)
|
||||
reinstalling.value = false
|
||||
@@ -226,9 +302,11 @@ provideInstallationSettings({
|
||||
loader: instance.value.loader,
|
||||
game_version: instance.value.game_version,
|
||||
})
|
||||
debug('reinstallModpack: done')
|
||||
},
|
||||
|
||||
async unlinkModpack() {
|
||||
debug('unlinkModpack: called', { instancePath: instance.value.path })
|
||||
await edit(instance.value.path, {
|
||||
linked_data: null as unknown as undefined,
|
||||
})
|
||||
@@ -236,27 +314,38 @@ provideInstallationSettings({
|
||||
queryKey: ['linkedModpackInfo', instance.value.path],
|
||||
})
|
||||
onUnlinked()
|
||||
debug('unlinkModpack: done')
|
||||
},
|
||||
|
||||
getCachedModpackVersions: () => null,
|
||||
async fetchModpackVersions() {
|
||||
debug('fetchModpackVersions: called', {
|
||||
projectId: instance.value.linked_data?.project_id,
|
||||
})
|
||||
const versions = await get_project_versions(instance.value.linked_data!.project_id!).catch(
|
||||
handleError,
|
||||
)
|
||||
debug('fetchModpackVersions: done', { count: versions?.length ?? 0 })
|
||||
return (versions ?? []) as Labrinth.Versions.v2.Version[]
|
||||
},
|
||||
|
||||
async getVersionChangelog(versionId: string) {
|
||||
debug('getVersionChangelog: called', { versionId })
|
||||
return (await get_version(versionId, 'must_revalidate').catch(
|
||||
() => null,
|
||||
)) as Labrinth.Versions.v2.Version | null
|
||||
},
|
||||
|
||||
async onModpackVersionConfirm(version) {
|
||||
debug('onModpackVersionConfirm: called', {
|
||||
versionId: version.id,
|
||||
instancePath: instance.value.path,
|
||||
})
|
||||
await update_managed_modrinth_version(instance.value.path, version.id)
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['linkedModpackInfo', instance.value.path],
|
||||
})
|
||||
debug('onModpackVersionConfirm: done')
|
||||
},
|
||||
|
||||
updaterModalProps: computed(() => ({
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CoffeeIcon,
|
||||
FolderSearchIcon,
|
||||
RefreshCwIcon,
|
||||
SearchIcon,
|
||||
SpinnerIcon,
|
||||
XCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
@@ -8,9 +17,11 @@ import {
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { computed, readonly, ref, watch } from 'vue'
|
||||
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||
import useJavaTest from '@/composables/useJavaTest'
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
@@ -25,9 +36,54 @@ const { instance } = injectInstanceSettings()
|
||||
|
||||
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
|
||||
|
||||
const overrideJavaInstall = ref(!!instance.value.java_path)
|
||||
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
|
||||
const javaInstall = ref({ path: optimalJava.path ?? instance.value.java_path })
|
||||
|
||||
const overrideJavaInstall = ref(!!instance.value.java_path)
|
||||
const javaPath = ref(instance.value.java_path ?? optimalJava?.path ?? '')
|
||||
|
||||
const activePath = computed(() =>
|
||||
overrideJavaInstall.value ? javaPath.value : (optimalJava?.path ?? ''),
|
||||
)
|
||||
|
||||
watch(overrideJavaInstall, (enabled) => {
|
||||
if (enabled && !javaPath.value) {
|
||||
javaPath.value = optimalJava?.path ?? ''
|
||||
}
|
||||
})
|
||||
|
||||
const { testingJava, javaTestResult, testJavaInstallationDebounced, testJavaInstallation } =
|
||||
useJavaTest()
|
||||
|
||||
const hoveringTest = ref(false)
|
||||
let hasInitialized = false
|
||||
|
||||
watch(
|
||||
activePath,
|
||||
(newPath) => {
|
||||
if (newPath && optimalJava?.parsed_version) {
|
||||
if (!hasInitialized) {
|
||||
testJavaInstallation(newPath, optimalJava?.parsed_version, false)
|
||||
hasInitialized = true
|
||||
} else {
|
||||
testJavaInstallationDebounced(newPath, optimalJava?.parsed_version)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const javaDetectionModal = ref<{ show: (version: number, current: object) => void } | null>(null)
|
||||
|
||||
async function handleBrowseJava() {
|
||||
const result = await open({ multiple: false })
|
||||
if (result) {
|
||||
javaPath.value = result
|
||||
}
|
||||
}
|
||||
|
||||
function handleDetectJava() {
|
||||
javaDetectionModal.value?.show(optimalJava?.parsed_version, { path: javaPath.value })
|
||||
}
|
||||
|
||||
const overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0)
|
||||
const javaArgs = ref(
|
||||
@@ -51,8 +107,8 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
|
||||
const editProfileObject = computed(() => {
|
||||
return {
|
||||
java_path:
|
||||
overrideJavaInstall.value && javaInstall.value.path !== ''
|
||||
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
|
||||
overrideJavaInstall.value && javaPath.value
|
||||
? javaPath.value.replace('java.exe', 'javaw.exe')
|
||||
: null,
|
||||
extra_launch_args: overrideJavaArgs.value
|
||||
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
|
||||
@@ -71,7 +127,7 @@ const editProfileObject = computed(() => {
|
||||
watch(
|
||||
[
|
||||
overrideJavaInstall,
|
||||
javaInstall,
|
||||
javaPath,
|
||||
overrideJavaArgs,
|
||||
javaArgs,
|
||||
overrideEnvVars,
|
||||
@@ -90,17 +146,45 @@ const messages = defineMessages({
|
||||
id: 'instance.settings.tabs.java.java-installation',
|
||||
defaultMessage: 'Java installation',
|
||||
},
|
||||
customJavaInstallation: {
|
||||
id: 'instance.settings.tabs.java.custom-java-installation',
|
||||
defaultMessage: 'Custom Java installation',
|
||||
},
|
||||
javaPathPlaceholder: {
|
||||
id: 'instance.settings.tabs.java.java-path-placeholder',
|
||||
defaultMessage: '/path/to/java',
|
||||
},
|
||||
javaMemory: {
|
||||
id: 'instance.settings.tabs.java.java-memory',
|
||||
defaultMessage: 'Memory allocated',
|
||||
},
|
||||
customMemoryAllocation: {
|
||||
id: 'instance.settings.tabs.java.custom-memory-allocation',
|
||||
defaultMessage: 'Custom memory allocation',
|
||||
},
|
||||
javaArguments: {
|
||||
id: 'instance.settings.tabs.java.java-arguments',
|
||||
defaultMessage: 'Java arguments',
|
||||
},
|
||||
customJavaArguments: {
|
||||
id: 'instance.settings.tabs.java.custom-java-arguments',
|
||||
defaultMessage: 'Custom Java arguments',
|
||||
},
|
||||
enterJavaArguments: {
|
||||
id: 'instance.settings.tabs.java.enter-java-arguments',
|
||||
defaultMessage: 'Enter Java arguments...',
|
||||
},
|
||||
javaEnvironmentVariables: {
|
||||
id: 'instance.settings.tabs.java.environment-variables',
|
||||
defaultMessage: 'Environment variables',
|
||||
},
|
||||
javaMemory: {
|
||||
id: 'instance.settings.tabs.java.java-memory',
|
||||
defaultMessage: 'Memory allocated',
|
||||
customEnvironmentVariables: {
|
||||
id: 'instance.settings.tabs.java.custom-environment-variables',
|
||||
defaultMessage: 'Custom environment variables',
|
||||
},
|
||||
enterEnvironmentVariables: {
|
||||
id: 'instance.settings.tabs.java.enter-environment-variables',
|
||||
defaultMessage: 'Enter environmental variables...',
|
||||
},
|
||||
hooks: {
|
||||
id: 'instance.settings.tabs.java.hooks',
|
||||
@@ -111,43 +195,86 @@ const messages = defineMessages({
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 id="project-name" class="m-0 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
<JavaDetectionModal ref="javaDetectionModal" @submit="(val) => (javaPath = val.path)" />
|
||||
<h2 class="m-0 mb-2 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaInstallation) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideJavaInstall" label="Custom Java installation" class="mb-2.5" />
|
||||
<template v-if="!overrideJavaInstall">
|
||||
<div class="flex my-2 items-center gap-2 font-semibold">
|
||||
<template v-if="javaInstall">
|
||||
<CheckCircleIcon class="text-brand-green h-4 w-4" />
|
||||
<span>Using default Java {{ optimalJava.major_version }} installation:</span>
|
||||
</template>
|
||||
<template v-else-if="optimalJava">
|
||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||
<span
|
||||
>Could not find a default Java {{ optimalJava.major_version }} installation. Please set
|
||||
one below:</span
|
||||
<Checkbox
|
||||
v-model="overrideJavaInstall"
|
||||
:label="formatMessage(messages.customJavaInstallation)"
|
||||
class="mb-2"
|
||||
/>
|
||||
<div class="flex gap-4 p-4 bg-bg rounded-2xl">
|
||||
<div class="flex gap-3 items-start flex-1 min-w-0">
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-button-bg border-solid border-[1px] border-button-border p-2 mt-1 shrink-0 [&_svg]:h-full [&_svg]:w-full"
|
||||
>
|
||||
<CoffeeIcon />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 flex-1 min-w-0">
|
||||
<span class="font-semibold leading-none mt-2"
|
||||
>Java {{ optimalJava?.parsed_version }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<XCircleIcon class="text-brand-red h-5 w-5" />
|
||||
<span
|
||||
>Could not automatically determine a Java installation to use. Please set one
|
||||
below:</span
|
||||
>
|
||||
</template>
|
||||
<div class="flex gap-2 items-center">
|
||||
<StyledInput
|
||||
:model-value="activePath"
|
||||
:disabled="!overrideJavaInstall"
|
||||
autocomplete="off"
|
||||
:placeholder="formatMessage(messages.javaPathPlaceholder)"
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
@update:model-value="(val) => (javaPath = String(val))"
|
||||
/>
|
||||
<ButtonStyled
|
||||
:color="
|
||||
!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
"
|
||||
color-fill="text"
|
||||
>
|
||||
<button
|
||||
:disabled="!overrideJavaInstall || testingJava"
|
||||
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
|
||||
@mouseenter="overrideJavaInstall && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="javaTestResult === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon v-else-if="javaTestResult !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="overrideJavaInstall" class="h-4 w-4" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="overrideJavaInstall" class="flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button @click="handleDetectJava">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="handleBrowseJava">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="javaInstall && !overrideJavaInstall"
|
||||
class="p-4 bg-bg rounded-xl text-xs text-secondary leading-none font-mono"
|
||||
>
|
||||
{{ javaInstall.path }}
|
||||
</div>
|
||||
</template>
|
||||
<JavaSelector v-if="overrideJavaInstall || !javaInstall" v-model="javaInstall" />
|
||||
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
</div>
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaMemory) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2.5" />
|
||||
<Checkbox
|
||||
v-model="overrideMemorySettings"
|
||||
:label="formatMessage(messages.customMemoryAllocation)"
|
||||
class="mb-2"
|
||||
/>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="memory.maximum"
|
||||
@@ -159,28 +286,36 @@ const messages = defineMessages({
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaArguments) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
|
||||
<Checkbox
|
||||
v-model="overrideJavaArgs"
|
||||
:label="formatMessage(messages.customJavaArguments)"
|
||||
class="my-2"
|
||||
/>
|
||||
<StyledInput
|
||||
id="java-args"
|
||||
v-model="javaArgs"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideJavaArgs"
|
||||
placeholder="Enter java arguments..."
|
||||
:placeholder="formatMessage(messages.enterJavaArguments)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
|
||||
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
{{ formatMessage(messages.javaEnvironmentVariables) }}
|
||||
</h2>
|
||||
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2.5" />
|
||||
<Checkbox
|
||||
v-model="overrideEnvVars"
|
||||
:label="formatMessage(messages.customEnvironmentVariables)"
|
||||
class="mb-2"
|
||||
/>
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="envVars"
|
||||
autocomplete="off"
|
||||
:disabled="!overrideEnvVars"
|
||||
placeholder="Enter environmental variables..."
|
||||
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
import { type MinecraftAuthError, minecraftAuthErrors } from './minecraft-auth-errors'
|
||||
import { findMinecraftAuthError, type MinecraftAuthError } from './minecraft-auth-errors'
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const rawError = ref<string>('')
|
||||
@@ -26,7 +26,7 @@ const loadingSignIn = ref(false)
|
||||
function show(errorVal: { message?: string }) {
|
||||
rawError.value = errorVal?.message ?? String(errorVal)
|
||||
|
||||
matchedError.value = minecraftAuthErrors.find((e) => rawError.value.includes(e.errorCode)) ?? null
|
||||
matchedError.value = findMinecraftAuthError(rawError.value)
|
||||
|
||||
debugCollapsed.value = true
|
||||
hide_ads_window()
|
||||
|
||||
+111
-1
@@ -1,10 +1,93 @@
|
||||
export interface MinecraftAuthError {
|
||||
errorCode: string
|
||||
errorCode?: string
|
||||
errorMatchers?: string[]
|
||||
matches?: (message: string) => boolean
|
||||
whatHappened: string
|
||||
stepsToFix: string[]
|
||||
}
|
||||
|
||||
export const minecraftAuthErrors: MinecraftAuthError[] = [
|
||||
{
|
||||
errorMatchers: ['Failed to deserialize response to JSON during step RefreshOAuthToken:'],
|
||||
whatHappened:
|
||||
'Your saved Microsoft sign-in token has expired or was revoked, so Modrinth App cannot refresh your Minecraft session.',
|
||||
stepsToFix: [
|
||||
'Sign out of the affected Minecraft account in Modrinth App',
|
||||
'Sign in to the account again',
|
||||
'Once the new sign-in finishes, try launching Minecraft again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorMatchers: ['Failed to deserialize response to JSON during step SisuAuthenticate:'],
|
||||
whatHappened:
|
||||
'Xbox services rejected the first sign-in response. This is most often caused by your system clock or time zone being out of sync.',
|
||||
stepsToFix: [
|
||||
'Open your system date and time settings',
|
||||
'Turn on automatic time zone and automatic time, if available',
|
||||
'Use the sync option in your system settings to synchronize the clock',
|
||||
'Restart Modrinth App',
|
||||
'Try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
matches: (message) =>
|
||||
message.includes('Failed to deserialize response to JSON during step MinecraftToken:') &&
|
||||
message.includes('429 Too Many Requests'),
|
||||
whatHappened:
|
||||
'Microsoft or Minecraft temporarily blocked the sign-in request because there were too many recent attempts.',
|
||||
stepsToFix: [
|
||||
'Wait about an hour before trying again',
|
||||
'Restart Modrinth App after waiting',
|
||||
'Try signing in once more',
|
||||
'If the same message appears, wait longer before retrying so the temporary limit can clear',
|
||||
],
|
||||
},
|
||||
{
|
||||
matches: (message) =>
|
||||
message.includes('Failed to deserialize response to JSON during step MinecraftToken:') &&
|
||||
/Status Code: 5\d\d/.test(message),
|
||||
whatHappened:
|
||||
"Minecraft's authentication service is returning a server error, so Modrinth App cannot finish signing you in right now.",
|
||||
stepsToFix: [
|
||||
'Wait a few minutes and try signing in again',
|
||||
'Check <a href="https://support.xbox.com/xbox-live-status">Xbox Status</a> for current service issues',
|
||||
'Try signing in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a> to confirm whether Minecraft sign-in is also affected there',
|
||||
'If the service is healthy and this keeps happening, contact support with the debug information below',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorMatchers: ['Failed to fetch player profile'],
|
||||
whatHappened:
|
||||
'Minecraft services could not return a Java Edition profile for this account. This most often happens when the game was purchased recently, the Java profile has not finished being created, or the wrong Microsoft account is being used.',
|
||||
stepsToFix: [
|
||||
'Sign in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>',
|
||||
'Launch Minecraft: Java Edition once from the official launcher',
|
||||
'Wait up to an hour if the purchase or profile setup was recent',
|
||||
'Make sure you are using the Microsoft account that owns Minecraft. See <a href="https://support.modrinth.com/en/articles/9409136-finding-the-right-xbox-account">Finding the right Xbox account</a> for help',
|
||||
'Try signing in to Modrinth App again',
|
||||
],
|
||||
},
|
||||
{
|
||||
matches: (message) =>
|
||||
message.includes('error sending request for url (') &&
|
||||
[
|
||||
'minecraft.net',
|
||||
'minecraftservices.com',
|
||||
'mojang.com',
|
||||
'xbox.com',
|
||||
'xboxlive.com',
|
||||
'live.com',
|
||||
].some((domain) => message.includes(domain)),
|
||||
whatHappened:
|
||||
'Modrinth App could not connect to a Microsoft, Xbox, or Minecraft service needed for sign-in. This is usually caused by a local network, DNS, proxy, firewall, hosts file, VPN, or antivirus issue.',
|
||||
stepsToFix: [
|
||||
'Restart Modrinth App and try signing in again',
|
||||
'Check that your internet connection is working',
|
||||
'Allow Modrinth App through your firewall, antivirus, proxy, VPN, and hosts file rules',
|
||||
'Try a different network or temporarily disable VPN/proxy software if you use one',
|
||||
'If routing or DNS is the issue, a service like Cloudflare WARP can sometimes help',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorCode: '2148916222',
|
||||
whatHappened:
|
||||
@@ -87,4 +170,31 @@ export const minecraftAuthErrors: MinecraftAuthError[] = [
|
||||
'Once finished, try signing in again',
|
||||
],
|
||||
},
|
||||
{
|
||||
errorMatchers: ['Failed to deserialize response to JSON during step XstsAuthorize:'],
|
||||
whatHappened:
|
||||
'Xbox services rejected the request to authorize this account for Minecraft services, but did not return a specific account restriction that Modrinth App recognizes.',
|
||||
stepsToFix: [
|
||||
'Sign in with the <a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>',
|
||||
'Complete any prompts shown by Microsoft, Xbox, or Minecraft',
|
||||
'Try signing in to Modrinth App again',
|
||||
'If the official launcher also fails, follow the error shown there or contact Xbox Support',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function findMinecraftAuthError(message: string): MinecraftAuthError | null {
|
||||
return (
|
||||
minecraftAuthErrors.find((error) => {
|
||||
if (error.errorCode && message.includes(error.errorCode)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (error.errorMatchers?.some((matcher) => message.includes(matcher))) {
|
||||
return true
|
||||
}
|
||||
|
||||
return error.matches?.(message) ?? false
|
||||
}) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="modal?.hide()">
|
||||
<button @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -45,12 +45,19 @@ const isMinecraftServer = ref(false)
|
||||
const handleUnlinked = () => emit('unlinked')
|
||||
|
||||
const instanceRef = computed(() => props.instance)
|
||||
const queryClient = useQueryClient()
|
||||
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||
|
||||
function hide() {
|
||||
tabbedModal.value?.hide()
|
||||
}
|
||||
|
||||
provideInstanceSettings({
|
||||
instance: instanceRef,
|
||||
offline: props.offline,
|
||||
isMinecraftServer,
|
||||
onUnlinked: handleUnlinked,
|
||||
closeModal: hide,
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -113,9 +120,6 @@ const tabs = computed<TabbedModalTab[]>(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||
|
||||
function show(tabIndex?: number) {
|
||||
if (props.instance.linked_data?.project_id) {
|
||||
queryClient.prefetchQuery({
|
||||
@@ -129,7 +133,7 @@ function show(tabIndex?: number) {
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
<template>
|
||||
<TabbedModal
|
||||
|
||||
@@ -31,7 +31,7 @@ const props = defineProps({
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
defineExpose({
|
||||
show: (e: MouseEvent) => {
|
||||
show: (e?: MouseEvent) => {
|
||||
modal.value?.show(e)
|
||||
},
|
||||
hide: () => {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="handleCancel">
|
||||
<button @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -1,13 +1,115 @@
|
||||
<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 worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
||||
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
||||
|
||||
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.",
|
||||
},
|
||||
showPlayTimeTitle: {
|
||||
id: 'app.appearance-settings.show-play-time.title',
|
||||
defaultMessage: 'Show play time',
|
||||
},
|
||||
showPlayTimeDescription: {
|
||||
id: 'app.appearance-settings.show-play-time.description',
|
||||
defaultMessage: `Displays how much time you've spent playing an instance.`,
|
||||
},
|
||||
})
|
||||
|
||||
const os = ref(await getOS())
|
||||
const settings = ref(await get())
|
||||
@@ -21,8 +123,10 @@ watch(
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-semibold 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="
|
||||
@@ -38,10 +142,11 @@ watch(
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold 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,57 +162,40 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Hide nametag</h2>
|
||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
||||
</div>
|
||||
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
||||
</div>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold 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-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold 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-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Default landing page</h2>
|
||||
<p class="m-0 mt-1">Change the page to which the launcher opens on.</p>
|
||||
</div>
|
||||
<Combobox
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="max-w-40"
|
||||
:options="['Home', 'Library'].map((v) => ({ value: v, label: v }))"
|
||||
:display-value="settings.default_page ?? 'Select an option'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">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.showPlayTimeTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
||||
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
|
||||
@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(showPlayTimeFlag)
|
||||
themeStore.featureFlags[showPlayTimeFlag] = newValue
|
||||
settings.feature_flags[showPlayTimeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
@@ -115,8 +203,94 @@ watch(
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold 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.hideNametagTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="hide-nametag-skins-page"
|
||||
:model-value="themeStore.hideNametagSkinsPage"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.hideNametagSkinsPage = !!e
|
||||
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.defaultLandingPageTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.defaultLandingPageDescription) }}</p>
|
||||
</div>
|
||||
<Combobox
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="max-w-40"
|
||||
:options="[
|
||||
{
|
||||
value: 'Home',
|
||||
label: formatMessage(messages.defaultLandingPageHome),
|
||||
},
|
||||
{
|
||||
value: 'Library',
|
||||
label: formatMessage(messages.defaultLandingPageLibrary),
|
||||
},
|
||||
]"
|
||||
:display-value="settings.default_page ?? 'Select an option'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
|
||||
themeStore.featureFlags[worldsInHomeFlag] = newValue
|
||||
settings.feature_flags[worldsInHomeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.unknownPackWarningTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.unknownPackWarningDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
const warnBeforeUnknownPackInstall = !!e
|
||||
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
|
||||
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
|
||||
settings.feature_flags[skipUnknownPackWarningFlag] = 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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
@@ -73,9 +73,11 @@ async function findLauncherDir() {
|
||||
wrapper-class="w-full"
|
||||
>
|
||||
<template #right>
|
||||
<Button class="ml-1.5" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
<ButtonStyled circular>
|
||||
<button class="ml-1.5" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</StyledInput>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
|
||||
@@ -1,146 +1,242 @@
|
||||
<template>
|
||||
<UploadSkinModal ref="uploadModal" />
|
||||
<ModalWrapper ref="modal" @on-hide="resetState">
|
||||
<NewModal ref="modal" :on-hide="handleModalHide">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
|
||||
{{ formatMessage(mode === 'edit' ? messages.editSkinTitle : messages.addSkinTitle) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin || ''"
|
||||
:cape-src="selectedCapeTexture"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-[25rem] w-[16rem] min-w-[16rem] flex-shrink-0 md:self-center">
|
||||
<SkinPreviewRenderer
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin || ''"
|
||||
:cape-src="selectedCapeTexture"
|
||||
framing="modal"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
||||
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
||||
<section v-if="mode === 'edit' && canEditTextureAndModel">
|
||||
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.textureSection) }}</h2>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" @click="openTextureFileBrowser">
|
||||
<UploadIcon /> {{ formatMessage(messages.replaceTextureButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
accept="image/png"
|
||||
class="hidden"
|
||||
@change="onTextureFileInputChange"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Arm style</h2>
|
||||
<section v-if="canEditTextureAndModel">
|
||||
<h2 class="text-base font-semibold mb-2">
|
||||
{{ formatMessage(messages.armStyleSection) }}
|
||||
</h2>
|
||||
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
|
||||
<template #default="{ item }">
|
||||
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
|
||||
{{
|
||||
formatMessage(item === 'CLASSIC' ? messages.wideArmStyle : messages.slimArmStyle)
|
||||
}}
|
||||
</template>
|
||||
</RadioButtons>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Cape</h2>
|
||||
<div class="flex gap-2">
|
||||
<CapeButton
|
||||
v-if="defaultCape"
|
||||
:id="defaultCape.id"
|
||||
:texture="defaultCape.texture"
|
||||
:name="undefined"
|
||||
:selected="!selectedCape"
|
||||
faded
|
||||
@select="selectCape(undefined)"
|
||||
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.capeSection) }}</h2>
|
||||
<div class="relative w-fit max-w-full">
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-200 ease-out"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-6"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 max-h-6"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
>
|
||||
<span>Use default cape</span>
|
||||
</CapeButton>
|
||||
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
|
||||
<span>Use default cape</span>
|
||||
</CapeLikeTextButton>
|
||||
<div
|
||||
v-if="showCapeTopFade"
|
||||
class="pointer-events-none absolute left-0 right-0 top-0 z-10 h-6 bg-gradient-to-b from-bg-raised to-transparent"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<CapeButton
|
||||
v-for="cape in visibleCapeList"
|
||||
:id="cape.id"
|
||||
:key="cape.id"
|
||||
:texture="cape.texture"
|
||||
:name="cape.name || 'Cape'"
|
||||
:selected="selectedCape?.id === cape.id"
|
||||
@select="selectCape(cape)"
|
||||
/>
|
||||
|
||||
<CapeLikeTextButton
|
||||
v-if="(capes?.length ?? 0) > 2"
|
||||
tooltip="View more capes"
|
||||
@mouseup="openSelectCapeModal"
|
||||
<div
|
||||
ref="capeListRef"
|
||||
class="grid grid-cols-[repeat(4,max-content)] auto-rows-max gap-2 overflow-y-auto pr-1"
|
||||
:style="{ maxHeight: capeListMaxHeight }"
|
||||
@scroll="checkCapeScrollState"
|
||||
>
|
||||
<template #icon><ChevronRightIcon /></template>
|
||||
<span>More</span>
|
||||
</CapeLikeTextButton>
|
||||
<CapeLikeTextButton
|
||||
:tooltip="formatMessage(messages.noCapeTooltip)"
|
||||
:highlighted="!selectedCape"
|
||||
@click="selectCape(undefined)"
|
||||
>
|
||||
<template #icon><XIcon /></template>
|
||||
<span>{{ formatMessage(messages.noneCapeOption) }}</span>
|
||||
</CapeLikeTextButton>
|
||||
|
||||
<CapeButton
|
||||
v-for="cape in sortedCapes"
|
||||
:id="cape.id"
|
||||
:key="cape.id"
|
||||
:texture="cape.texture"
|
||||
:name="cape.name || formatMessage(messages.capeFallbackName)"
|
||||
:selected="selectedCape?.id === cape.id"
|
||||
@select="selectCape(cape)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-200 ease-out"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-6"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 max-h-6"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
>
|
||||
<div
|
||||
v-if="showCapeBottomFade"
|
||||
class="pointer-events-none absolute bottom-0 left-0 right-0 z-10 h-6 bg-gradient-to-t from-bg-raised to-transparent"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-12">
|
||||
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
<SelectCapeModal
|
||||
ref="selectCapeModal"
|
||||
:capes="capes || []"
|
||||
@select="handleCapeSelected"
|
||||
@cancel="handleCapeCancel"
|
||||
/>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button :disabled="isSaving" @click="hide">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ formatMessage(mode === 'new' ? messages.addSkinButton : messages.saveSkinButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, SaveIcon, SpinnerIcon, UploadIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
SaveIcon,
|
||||
SpinnerIcon,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
RadioButtons,
|
||||
SkinPreviewRenderer,
|
||||
useScrollIndicator,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||
import {
|
||||
add_and_equip_custom_skin,
|
||||
type Cape,
|
||||
determineModelType,
|
||||
equip_skin,
|
||||
get_normalized_skin_texture,
|
||||
remove_custom_skin,
|
||||
normalize_skin_texture,
|
||||
save_custom_skin,
|
||||
type Skin,
|
||||
type SkinModel,
|
||||
type SkinTextureUrl,
|
||||
unequip_skin,
|
||||
} from '@/helpers/skins.ts'
|
||||
|
||||
const CAPE_LIST_MAX_HEIGHT = 334
|
||||
const messages = defineMessages({
|
||||
editSkinTitle: {
|
||||
id: 'app.skins.modal.edit-title',
|
||||
defaultMessage: 'Editing skin',
|
||||
},
|
||||
addSkinTitle: {
|
||||
id: 'app.skins.modal.add-title',
|
||||
defaultMessage: 'Adding a skin',
|
||||
},
|
||||
textureSection: {
|
||||
id: 'app.skins.modal.texture-section',
|
||||
defaultMessage: 'Texture',
|
||||
},
|
||||
replaceTextureButton: {
|
||||
id: 'app.skins.modal.replace-texture-button',
|
||||
defaultMessage: 'Replace texture',
|
||||
},
|
||||
armStyleSection: {
|
||||
id: 'app.skins.modal.arm-style-section',
|
||||
defaultMessage: 'Arm style',
|
||||
},
|
||||
wideArmStyle: {
|
||||
id: 'app.skins.modal.arm-style-wide',
|
||||
defaultMessage: 'Wide',
|
||||
},
|
||||
slimArmStyle: {
|
||||
id: 'app.skins.modal.arm-style-slim',
|
||||
defaultMessage: 'Slim',
|
||||
},
|
||||
capeSection: {
|
||||
id: 'app.skins.modal.cape-section',
|
||||
defaultMessage: 'Cape',
|
||||
},
|
||||
noCapeTooltip: {
|
||||
id: 'app.skins.modal.no-cape-tooltip',
|
||||
defaultMessage: 'No cape',
|
||||
},
|
||||
noneCapeOption: {
|
||||
id: 'app.skins.modal.none-cape-option',
|
||||
defaultMessage: 'None',
|
||||
},
|
||||
capeFallbackName: {
|
||||
id: 'app.skins.modal.cape-fallback-name',
|
||||
defaultMessage: 'Cape',
|
||||
},
|
||||
savingTooltip: {
|
||||
id: 'app.skins.modal.saving-tooltip',
|
||||
defaultMessage: 'Saving...',
|
||||
},
|
||||
uploadSkinFirstTooltip: {
|
||||
id: 'app.skins.modal.upload-skin-first-tooltip',
|
||||
defaultMessage: 'Upload a skin first!',
|
||||
},
|
||||
makeEditFirstTooltip: {
|
||||
id: 'app.skins.modal.make-edit-first-tooltip',
|
||||
defaultMessage: 'Make an edit to the skin first!',
|
||||
},
|
||||
addSkinButton: {
|
||||
id: 'app.skins.modal.add-skin-button',
|
||||
defaultMessage: 'Add skin',
|
||||
},
|
||||
saveSkinButton: {
|
||||
id: 'app.skins.modal.save-skin-button',
|
||||
defaultMessage: 'Save skin',
|
||||
},
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
const textureFileInput = useTemplateRef<HTMLInputElement>('textureFileInput')
|
||||
const capeListRef = ref<HTMLElement | null>(null)
|
||||
const capeListMaxHeight = ref(`${CAPE_LIST_MAX_HEIGHT}px`)
|
||||
const mode = ref<'new' | 'edit'>('new')
|
||||
const currentSkin = ref<Skin | null>(null)
|
||||
const shouldRestoreModal = ref(false)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const uploadedTextureUrl = ref<SkinTextureUrl | null>(null)
|
||||
@@ -148,10 +244,49 @@ const previewSkin = ref<string>('')
|
||||
|
||||
const variant = ref<SkinModel>('CLASSIC')
|
||||
const selectedCape = ref<Cape | undefined>(undefined)
|
||||
const props = defineProps<{ capes?: Cape[]; defaultCape?: Cape }>()
|
||||
const props = defineProps<{ capes?: Cape[] }>()
|
||||
|
||||
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
|
||||
const visibleCapeList = ref<Cape[]>([])
|
||||
const canEditTextureAndModel = computed(() => currentSkin.value?.source !== 'default')
|
||||
const {
|
||||
showTopFade: showCapeTopFade,
|
||||
showBottomFade: showCapeBottomFade,
|
||||
checkScrollState: checkCapeScrollState,
|
||||
forceCheck: forceCapeScrollCheck,
|
||||
} = useScrollIndicator(capeListRef)
|
||||
|
||||
let capeListLayoutFrame: number | null = null
|
||||
function updateCapeListLayout() {
|
||||
const capeList = capeListRef.value
|
||||
const modalContent = capeList?.closest('[data-modal-content]') as HTMLElement | null
|
||||
|
||||
if (!capeList || !modalContent) {
|
||||
capeListMaxHeight.value = `${CAPE_LIST_MAX_HEIGHT}px`
|
||||
forceCapeScrollCheck()
|
||||
return
|
||||
}
|
||||
|
||||
const availableHeight =
|
||||
modalContent.getBoundingClientRect().bottom - capeList.getBoundingClientRect().top
|
||||
|
||||
capeListMaxHeight.value = `${Math.min(
|
||||
CAPE_LIST_MAX_HEIGHT,
|
||||
Math.max(0, Math.floor(availableHeight)),
|
||||
)}px`
|
||||
|
||||
nextTick(() => forceCapeScrollCheck())
|
||||
}
|
||||
|
||||
function refreshCapeListLayout() {
|
||||
if (capeListLayoutFrame !== null) {
|
||||
cancelAnimationFrame(capeListLayoutFrame)
|
||||
}
|
||||
|
||||
capeListLayoutFrame = requestAnimationFrame(() => {
|
||||
capeListLayoutFrame = null
|
||||
updateCapeListLayout()
|
||||
})
|
||||
}
|
||||
|
||||
const sortedCapes = computed(() => {
|
||||
return [...(props.capes || [])].sort((a, b) => {
|
||||
@@ -161,32 +296,6 @@ const sortedCapes = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
function initVisibleCapeList() {
|
||||
if (!props.capes || props.capes.length === 0) {
|
||||
visibleCapeList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (visibleCapeList.value.length === 0) {
|
||||
if (selectedCape.value) {
|
||||
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
|
||||
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
|
||||
} else {
|
||||
visibleCapeList.value = getSortedCapes(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSortedCapes(count: number): Cape[] {
|
||||
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
|
||||
return sortedCapes.value.slice(0, count)
|
||||
}
|
||||
|
||||
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
||||
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
|
||||
return sortedCapes.value.find((cape) => cape.id !== excludeId)
|
||||
}
|
||||
|
||||
async function loadPreviewSkin() {
|
||||
if (uploadedTextureUrl.value) {
|
||||
previewSkin.value = uploadedTextureUrl.value.normalized
|
||||
@@ -218,9 +327,13 @@ const disableSave = computed(
|
||||
)
|
||||
|
||||
const saveTooltip = computed(() => {
|
||||
if (isSaving.value) return 'Saving...'
|
||||
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
||||
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
||||
if (isSaving.value) return formatMessage(messages.savingTooltip)
|
||||
if (mode.value === 'new' && !uploadedTextureUrl.value) {
|
||||
return formatMessage(messages.uploadSkinFirstTooltip)
|
||||
}
|
||||
if (mode.value === 'edit' && !hasEdits.value) {
|
||||
return formatMessage(messages.makeEditFirstTooltip)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
@@ -231,11 +344,13 @@ function resetState() {
|
||||
previewSkin.value = ''
|
||||
variant.value = 'CLASSIC'
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
shouldRestoreModal.value = false
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
function handleModalHide() {
|
||||
setTimeout(() => resetState(), 250)
|
||||
}
|
||||
|
||||
async function show(e: MouseEvent, skin?: Skin) {
|
||||
mode.value = skin ? 'edit' : 'new'
|
||||
currentSkin.value = skin ?? null
|
||||
@@ -246,12 +361,11 @@ async function show(e: MouseEvent, skin?: Skin) {
|
||||
variant.value = 'CLASSIC'
|
||||
selectedCape.value = undefined
|
||||
}
|
||||
visibleCapeList.value = []
|
||||
initVisibleCapeList()
|
||||
|
||||
await loadPreviewSkin()
|
||||
|
||||
modal.value?.show(e)
|
||||
nextTick(() => refreshCapeListLayout())
|
||||
}
|
||||
|
||||
async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
|
||||
@@ -260,98 +374,54 @@ async function showNew(e: MouseEvent, skinTextureUrl: SkinTextureUrl) {
|
||||
uploadedTextureUrl.value = skinTextureUrl
|
||||
variant.value = await determineModelType(skinTextureUrl.original)
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
initVisibleCapeList()
|
||||
|
||||
await loadPreviewSkin()
|
||||
|
||||
modal.value?.show(e)
|
||||
nextTick(() => refreshCapeListLayout())
|
||||
}
|
||||
|
||||
async function restoreWithNewTexture(skinTextureUrl: SkinTextureUrl) {
|
||||
async function setUploadedTexture(skinTextureUrl: SkinTextureUrl) {
|
||||
uploadedTextureUrl.value = skinTextureUrl
|
||||
await loadPreviewSkin()
|
||||
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
nextTick(() => refreshCapeListLayout())
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
setTimeout(() => resetState(), 250)
|
||||
}
|
||||
|
||||
function selectCape(cape: Cape | undefined) {
|
||||
if (cape && selectedCape.value?.id !== cape.id) {
|
||||
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
|
||||
if (!isInVisibleList && visibleCapeList.value.length > 0) {
|
||||
visibleCapeList.value.splice(0, 1, cape)
|
||||
|
||||
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
|
||||
const otherCape = getSortedCapeExcluding(cape.id)
|
||||
if (otherCape) {
|
||||
visibleCapeList.value.splice(1, 1, otherCape)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedCape.value = cape
|
||||
}
|
||||
|
||||
function handleCapeSelected(cape: Cape | undefined) {
|
||||
selectCape(cape)
|
||||
function openTextureFileBrowser() {
|
||||
textureFileInput.value?.click()
|
||||
}
|
||||
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
async function onTextureFileInputChange(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
const file = files?.[0]
|
||||
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function handleCapeCancel() {
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function openSelectCapeModal(e: MouseEvent) {
|
||||
if (!selectCapeModal.value) return
|
||||
|
||||
shouldRestoreModal.value = true
|
||||
modal.value?.hide()
|
||||
|
||||
setTimeout(() => {
|
||||
selectCapeModal.value?.show(
|
||||
e,
|
||||
currentSkin.value?.texture_key,
|
||||
selectedCape.value,
|
||||
previewSkin.value,
|
||||
variant.value,
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function openUploadSkinModal(e: MouseEvent) {
|
||||
shouldRestoreModal.value = true
|
||||
modal.value?.hide()
|
||||
emit('open-upload-modal', e)
|
||||
}
|
||||
|
||||
function restoreModal() {
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
const fakeEvent = new MouseEvent('click')
|
||||
modal.value?.show(fakeEvent)
|
||||
shouldRestoreModal.value = false
|
||||
}, 500)
|
||||
try {
|
||||
const originalSkinTexUrl = `data:image/png;base64,${arrayBufferToBase64(
|
||||
await file.arrayBuffer(),
|
||||
)}`
|
||||
const skinTextureNormalized = await normalize_skin_texture(originalSkinTexUrl)
|
||||
await setUploadedTexture({
|
||||
original: originalSkinTexUrl,
|
||||
normalized: `data:image/png;base64,${arrayBufferToBase64(skinTextureNormalized)}`,
|
||||
})
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
if (textureFileInput.value) {
|
||||
textureFileInput.value.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,17 +437,32 @@ async function save() {
|
||||
textureUrl = currentSkin.value!.texture
|
||||
}
|
||||
|
||||
await unequip_skin()
|
||||
|
||||
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
|
||||
|
||||
if (mode.value === 'new') {
|
||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||
emit('saved')
|
||||
const addedSkin = await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||
emit('saved', {
|
||||
applied: true,
|
||||
skin: addedSkin,
|
||||
})
|
||||
} else {
|
||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||
await remove_custom_skin(currentSkin.value!)
|
||||
emit('saved')
|
||||
const updatedSkin = await save_custom_skin(
|
||||
currentSkin.value!,
|
||||
bytes,
|
||||
variant.value,
|
||||
selectedCape.value,
|
||||
!!uploadedTextureUrl.value && textureUrl !== currentSkin.value?.texture,
|
||||
)
|
||||
|
||||
if (currentSkin.value?.is_equipped) {
|
||||
await equip_skin(updatedSkin)
|
||||
}
|
||||
|
||||
emit('saved', {
|
||||
applied: !!currentSkin.value?.is_equipped,
|
||||
skin: updatedSkin,
|
||||
previousSkin: currentSkin.value!,
|
||||
})
|
||||
}
|
||||
|
||||
hide()
|
||||
@@ -390,28 +475,53 @@ async function save() {
|
||||
|
||||
watch([uploadedTextureUrl, currentSkin], async () => {
|
||||
await loadPreviewSkin()
|
||||
refreshCapeListLayout()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.capes,
|
||||
() => {
|
||||
initVisibleCapeList()
|
||||
nextTick(() => refreshCapeListLayout())
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
capeListRef,
|
||||
(capeList, _, onCleanup) => {
|
||||
if (!capeList) return
|
||||
|
||||
const modalContent = capeList.closest('[data-modal-content]')
|
||||
const resizeObserver = new ResizeObserver(() => refreshCapeListLayout())
|
||||
|
||||
if (modalContent instanceof HTMLElement) {
|
||||
resizeObserver.observe(modalContent)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', refreshCapeListLayout, { passive: true })
|
||||
refreshCapeListLayout()
|
||||
|
||||
onCleanup(() => {
|
||||
resizeObserver.disconnect()
|
||||
window.removeEventListener('resize', refreshCapeListLayout)
|
||||
|
||||
if (capeListLayoutFrame !== null) {
|
||||
cancelAnimationFrame(capeListLayoutFrame)
|
||||
capeListLayoutFrame = null
|
||||
}
|
||||
})
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'saved'): void
|
||||
(event: 'saved', options: { applied: boolean; skin?: Skin; previousSkin?: Skin }): void
|
||||
(event: 'deleted', skin: Skin): void
|
||||
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
|
||||
}>()
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
showNew,
|
||||
restoreWithNewTexture,
|
||||
hide,
|
||||
shouldRestoreModal,
|
||||
restoreModal,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
ScrollablePanel,
|
||||
SkinPreviewRenderer,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', cape: Cape | undefined): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
capes: Cape[]
|
||||
}>()
|
||||
|
||||
const sortedCapes = computed(() => {
|
||||
return [...props.capes].sort((a, b) => {
|
||||
const nameA = (a.name || '').toLowerCase()
|
||||
const nameB = (b.name || '').toLowerCase()
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
})
|
||||
|
||||
const currentSkinId = ref<string | undefined>()
|
||||
const currentSkinTexture = ref<string | undefined>()
|
||||
const currentSkinVariant = ref<SkinModel>('CLASSIC')
|
||||
const currentCapeTexture = computed<string | undefined>(() => currentCape.value?.texture)
|
||||
const currentCape = ref<Cape | undefined>()
|
||||
|
||||
function show(
|
||||
e: MouseEvent,
|
||||
skinId?: string,
|
||||
selected?: Cape,
|
||||
skinTexture?: string,
|
||||
variant?: SkinModel,
|
||||
) {
|
||||
currentSkinId.value = skinId
|
||||
currentSkinTexture.value = skinTexture
|
||||
currentSkinVariant.value = variant || 'CLASSIC'
|
||||
currentCape.value = selected
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
function select() {
|
||||
emit('select', currentCape.value)
|
||||
hide()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function updateSelectedCape(cape: Cape | undefined) {
|
||||
currentCape.value = cape
|
||||
}
|
||||
|
||||
function onModalHide() {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper ref="modal" @on-hide="onModalHide">
|
||||
<template #title>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-lg font-extrabold text-heading">Change cape</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
v-if="currentSkinTexture"
|
||||
:cape-src="currentCapeTexture"
|
||||
:texture-src="currentSkinTexture"
|
||||
:variant="currentSkinVariant"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
:initial-rotation="Math.PI + Math.PI / 8"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full my-auto">
|
||||
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
|
||||
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
|
||||
<CapeLikeTextButton
|
||||
tooltip="No Cape"
|
||||
:highlighted="!currentCape"
|
||||
@click="updateSelectedCape(undefined)"
|
||||
>
|
||||
<template #icon>
|
||||
<XIcon />
|
||||
</template>
|
||||
<span>None</span>
|
||||
</CapeLikeTextButton>
|
||||
<CapeButton
|
||||
v-for="cape in sortedCapes"
|
||||
:id="cape.id"
|
||||
:key="cape.id"
|
||||
:name="cape.name"
|
||||
:texture="cape.texture"
|
||||
:selected="currentCape?.id === cape.id"
|
||||
@select="updateSelectedCape(cape)"
|
||||
/>
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="select">
|
||||
<CheckIcon />
|
||||
Select
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<ModalWrapper ref="modal" @on-hide="hide(true)">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
|
||||
</template>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
|
||||
<UploadIcon /> Select skin texture file
|
||||
</p>
|
||||
<p class="mx-auto mt-0 text-secondary text-sm text-center">
|
||||
Drag and drop or click here to browse
|
||||
</p>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/png"
|
||||
class="hidden"
|
||||
@change="handleInputFileChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UploadIcon } from '@modrinth/assets'
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get_dragged_skin_data } from '@/helpers/skins'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const modal = ref()
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const unlisten = ref<() => void>()
|
||||
const modalVisible = ref(false)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'uploaded', data: ArrayBuffer): void
|
||||
(e: 'canceled'): void
|
||||
}>()
|
||||
|
||||
function show(e?: MouseEvent) {
|
||||
modal.value?.show(e)
|
||||
modalVisible.value = true
|
||||
setupDragDropListener()
|
||||
}
|
||||
|
||||
function hide(emitCanceled = false) {
|
||||
modal.value?.hide()
|
||||
modalVisible.value = false
|
||||
cleanupDragDropListener()
|
||||
resetState()
|
||||
if (emitCanceled) {
|
||||
emit('canceled')
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
}
|
||||
|
||||
function triggerFileInput() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleInputFileChange(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files || files.length === 0) {
|
||||
return
|
||||
}
|
||||
const file = files[0]
|
||||
const buffer = await file.arrayBuffer()
|
||||
await processData(buffer)
|
||||
}
|
||||
|
||||
async function setupDragDropListener() {
|
||||
try {
|
||||
if (modalVisible.value) {
|
||||
await cleanupDragDropListener()
|
||||
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
if (event.payload.type !== 'drop') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.payload.paths || event.payload.paths.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const filePath = event.payload.paths[0]
|
||||
|
||||
try {
|
||||
const data = await get_dragged_skin_data(filePath)
|
||||
await processData(data.buffer)
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
title: 'Error processing file',
|
||||
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set up drag and drop listener:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupDragDropListener() {
|
||||
if (unlisten.value) {
|
||||
unlisten.value()
|
||||
unlisten.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function processData(buffer: ArrayBuffer) {
|
||||
emit('uploaded', buffer)
|
||||
hide()
|
||||
}
|
||||
|
||||
watch(modalVisible, (isVisible) => {
|
||||
if (isVisible) {
|
||||
setupDragDropListener()
|
||||
} else {
|
||||
cleanupDragDropListener()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupDragDropListener()
|
||||
})
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
@@ -0,0 +1,432 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
SkinButton,
|
||||
SkinLikeTextButton,
|
||||
useScrollViewport,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useElementSize, useWindowSize } from '@vueuse/core'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import type { Skin } from '@/helpers/skins.ts'
|
||||
|
||||
type SkinSectionKind = 'saved' | 'default'
|
||||
type SkinLikeTextButtonExpose = {
|
||||
getRootElement: () => HTMLElement | null | undefined
|
||||
}
|
||||
type AddSkinButtonRef = SkinLikeTextButtonExpose | SkinLikeTextButtonExpose[]
|
||||
|
||||
interface DefaultSkinSection {
|
||||
title: string
|
||||
infoTooltip?: string
|
||||
skins: Skin[]
|
||||
}
|
||||
|
||||
interface SkinSection {
|
||||
key: string
|
||||
title: string
|
||||
kind: SkinSectionKind
|
||||
infoTooltip?: string
|
||||
skins: Skin[]
|
||||
}
|
||||
|
||||
interface VirtualSkinSection {
|
||||
section: SkinSection
|
||||
top: number
|
||||
index: number
|
||||
}
|
||||
|
||||
const SKIN_CARD_ASPECT_WIDTH = 31
|
||||
const SKIN_CARD_ASPECT_HEIGHT = 40
|
||||
const SKIN_GRID_GAP = 12
|
||||
const SKIN_SECTION_FIRST_SPACING = 4
|
||||
const SKIN_SECTION_SPACING = 24
|
||||
const SKIN_SECTION_HEADER_HEIGHT = 28
|
||||
const SKIN_SECTION_CONTENT_SPACING = 8
|
||||
const SKIN_SECTION_OVERSCAN = 900
|
||||
const FALLBACK_CARD_WIDTH = 220
|
||||
const messages = defineMessages({
|
||||
savedSkinsSection: {
|
||||
id: 'app.skins.section.saved-skins',
|
||||
defaultMessage: 'Saved skins',
|
||||
},
|
||||
addSkinButton: {
|
||||
id: 'app.skins.add-button',
|
||||
defaultMessage: 'Add skin',
|
||||
},
|
||||
dragAndDropSubtitle: {
|
||||
id: 'app.skins.add-button.drag-and-drop',
|
||||
defaultMessage: 'Drag and drop',
|
||||
},
|
||||
editSkinButton: {
|
||||
id: 'app.skins.edit-button',
|
||||
defaultMessage: 'Edit skin',
|
||||
},
|
||||
deleteSkinButton: {
|
||||
id: 'app.skins.delete-button',
|
||||
defaultMessage: 'Delete skin',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
savedSkins: Skin[]
|
||||
defaultSkinSections: DefaultSkinSection[]
|
||||
getBakedSkinTextures: (skin: Skin) => RenderResult | undefined
|
||||
isSkinSelected: (skin: Skin) => boolean
|
||||
isSkinActive: (skin: Skin) => boolean
|
||||
isAddSkinButtonDragActive: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [skin: Skin]
|
||||
edit: [skin: Skin, event: MouseEvent]
|
||||
delete: [skin: Skin]
|
||||
'add-skin': []
|
||||
'add-skin-dragenter': [event: DragEvent]
|
||||
'add-skin-dragover': [event: DragEvent]
|
||||
'add-skin-dragleave': [event: DragEvent]
|
||||
'add-skin-drop': [event: DragEvent]
|
||||
}>()
|
||||
|
||||
const addSkinButton = useTemplateRef<AddSkinButtonRef>('addSkinButton')
|
||||
const { formatMessage } = useVIntl()
|
||||
const { listContainer, relativeScrollTop, scrollContainer, viewportHeight } = useScrollViewport()
|
||||
const openSectionKeys = ref<Set<string>>(new Set())
|
||||
const hasSettledInitialLayout = ref(false)
|
||||
const knownSectionKeys = new Set<string>()
|
||||
let enableLayoutTransitionsFrame: number | null = null
|
||||
let isEnableLayoutTransitionsScheduled = false
|
||||
let isUnmounted = false
|
||||
|
||||
const { width: listWidth } = useElementSize(listContainer)
|
||||
const { width: windowWidth } = useWindowSize()
|
||||
|
||||
const columnCount = computed(() => {
|
||||
if (windowWidth.value >= 2050) {
|
||||
return 6
|
||||
}
|
||||
|
||||
if (windowWidth.value >= 1750) {
|
||||
return 5
|
||||
}
|
||||
|
||||
if (windowWidth.value >= 1300) {
|
||||
return 4
|
||||
}
|
||||
|
||||
return 3
|
||||
})
|
||||
|
||||
const cardWidth = computed(() => {
|
||||
if (listWidth.value <= 0) {
|
||||
return FALLBACK_CARD_WIDTH
|
||||
}
|
||||
|
||||
const gapsWidth = (columnCount.value - 1) * SKIN_GRID_GAP
|
||||
return Math.max(0, (listWidth.value - gapsWidth) / columnCount.value)
|
||||
})
|
||||
|
||||
const cardHeight = computed(
|
||||
() => (cardWidth.value * SKIN_CARD_ASPECT_HEIGHT) / SKIN_CARD_ASPECT_WIDTH,
|
||||
)
|
||||
|
||||
const sections = computed<SkinSection[]>(() => [
|
||||
{
|
||||
key: 'saved-skins',
|
||||
title: formatMessage(messages.savedSkinsSection),
|
||||
kind: 'saved',
|
||||
skins: props.savedSkins,
|
||||
},
|
||||
...props.defaultSkinSections.map((section) => ({
|
||||
key: defaultSkinSectionKey(section.title),
|
||||
title: section.title,
|
||||
kind: 'default' as const,
|
||||
infoTooltip: section.infoTooltip,
|
||||
skins: section.skins,
|
||||
})),
|
||||
])
|
||||
|
||||
const sectionLayouts = computed(() => {
|
||||
const layouts: Array<{ section: SkinSection; top: number; height: number; index: number }> = []
|
||||
let top = 0
|
||||
|
||||
sections.value.forEach((section, index) => {
|
||||
const height = getSectionHeightEstimate(section, index)
|
||||
layouts.push({ section, top, height, index })
|
||||
top += height
|
||||
})
|
||||
|
||||
return layouts
|
||||
})
|
||||
|
||||
const totalHeight = computed(() => {
|
||||
const lastSection = sectionLayouts.value[sectionLayouts.value.length - 1]
|
||||
return lastSection ? lastSection.top + lastSection.height : 0
|
||||
})
|
||||
|
||||
const visibleSections = computed<VirtualSkinSection[]>(() => {
|
||||
if (!listContainer.value || !scrollContainer.value) {
|
||||
return sectionLayouts.value.slice(0, 4)
|
||||
}
|
||||
|
||||
const viewportStart = Math.max(0, relativeScrollTop.value - SKIN_SECTION_OVERSCAN)
|
||||
const viewportEnd = relativeScrollTop.value + viewportHeight.value + SKIN_SECTION_OVERSCAN
|
||||
|
||||
return sectionLayouts.value
|
||||
.filter((layout) => layout.top + layout.height >= viewportStart && layout.top <= viewportEnd)
|
||||
.map(({ section, top, index }) => ({ section, top, index }))
|
||||
})
|
||||
|
||||
watch(
|
||||
sections,
|
||||
(nextSections) => {
|
||||
const sectionKeys = new Set(nextSections.map((section) => section.key))
|
||||
const openKeys = new Set(openSectionKeys.value)
|
||||
|
||||
for (const section of nextSections) {
|
||||
if (!knownSectionKeys.has(section.key)) {
|
||||
knownSectionKeys.add(section.key)
|
||||
openKeys.add(section.key)
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of knownSectionKeys) {
|
||||
if (!sectionKeys.has(key)) {
|
||||
knownSectionKeys.delete(key)
|
||||
openKeys.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
openSectionKeys.value = openKeys
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
listWidth,
|
||||
(width) => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
width <= 0 ||
|
||||
hasSettledInitialLayout.value ||
|
||||
isEnableLayoutTransitionsScheduled
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
isEnableLayoutTransitionsScheduled = true
|
||||
void nextTick(() => {
|
||||
if (isUnmounted) return
|
||||
|
||||
enableLayoutTransitionsFrame = window.requestAnimationFrame(() => {
|
||||
if (isUnmounted) return
|
||||
|
||||
enableLayoutTransitionsFrame = window.requestAnimationFrame(() => {
|
||||
if (isUnmounted) return
|
||||
|
||||
hasSettledInitialLayout.value = true
|
||||
enableLayoutTransitionsFrame = null
|
||||
isEnableLayoutTransitionsScheduled = false
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
isUnmounted = true
|
||||
|
||||
if (enableLayoutTransitionsFrame !== null) {
|
||||
window.cancelAnimationFrame(enableLayoutTransitionsFrame)
|
||||
}
|
||||
})
|
||||
|
||||
function defaultSkinSectionKey(title: string) {
|
||||
return `default-skins-${title}`
|
||||
}
|
||||
|
||||
function skinKey(skin: Skin, prefix: string) {
|
||||
return `${prefix}-${skin.source}-${skin.texture_key}-${skin.variant}-${skin.cape_id ?? 'no-cape'}`
|
||||
}
|
||||
|
||||
function isSectionOpen(key: string) {
|
||||
return openSectionKeys.value.has(key)
|
||||
}
|
||||
|
||||
function setSectionOpen(key: string, open: boolean) {
|
||||
const openKeys = new Set(openSectionKeys.value)
|
||||
|
||||
if (open) {
|
||||
openKeys.add(key)
|
||||
} else {
|
||||
openKeys.delete(key)
|
||||
}
|
||||
|
||||
openSectionKeys.value = openKeys
|
||||
}
|
||||
|
||||
function getSectionHeightEstimate(section: SkinSection, index: number) {
|
||||
const spacing = index === 0 ? SKIN_SECTION_FIRST_SPACING : SKIN_SECTION_SPACING
|
||||
|
||||
if (!isSectionOpen(section.key)) {
|
||||
return spacing + SKIN_SECTION_HEADER_HEIGHT
|
||||
}
|
||||
|
||||
const cardCount = section.kind === 'saved' ? section.skins.length + 1 : section.skins.length
|
||||
const rowCount = Math.ceil(cardCount / columnCount.value)
|
||||
const gridHeight = rowCount * cardHeight.value + Math.max(0, rowCount - 1) * SKIN_GRID_GAP
|
||||
|
||||
return spacing + SKIN_SECTION_HEADER_HEIGHT + SKIN_SECTION_CONTENT_SPACING + gridHeight
|
||||
}
|
||||
|
||||
function getAddSkinButtonElement() {
|
||||
const button = Array.isArray(addSkinButton.value)
|
||||
? addSkinButton.value.find((candidate) => candidate.getRootElement())
|
||||
: addSkinButton.value
|
||||
|
||||
return button?.getRootElement()
|
||||
}
|
||||
|
||||
defineExpose({ getAddSkinButtonElement })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="listContainer"
|
||||
class="relative w-full"
|
||||
:style="{ height: `${totalHeight}px`, overflowAnchor: 'none' }"
|
||||
>
|
||||
<div
|
||||
v-for="{ section, top, index } in visibleSections"
|
||||
:key="section.key"
|
||||
class="absolute inset-x-0"
|
||||
:class="[
|
||||
index === 0 ? 'pt-1' : 'pt-6',
|
||||
hasSettledInitialLayout
|
||||
? 'transition-transform duration-300 ease-in-out will-change-transform motion-reduce:transition-none'
|
||||
: '',
|
||||
]"
|
||||
:style="{ transform: `translateY(${top}px)` }"
|
||||
>
|
||||
<Accordion
|
||||
button-class="group flex w-full items-center gap-[6px] bg-transparent m-0 p-0 border-none cursor-pointer text-left"
|
||||
content-class="pt-2"
|
||||
:open-by-default="isSectionOpen(section.key)"
|
||||
@on-open="setSectionOpen(section.key, true)"
|
||||
@on-close="setSectionOpen(section.key, false)"
|
||||
>
|
||||
<template #title>
|
||||
{{ section.title }}
|
||||
</template>
|
||||
<template #button="{ open }">
|
||||
<DropdownIcon
|
||||
class="size-6 shrink-0 text-primary transition-transform duration-300"
|
||||
:class="{ 'rotate-180': open }"
|
||||
/>
|
||||
<span class="min-w-0 text-xl font-semibold leading-7 text-primary">
|
||||
{{ section.title }}
|
||||
</span>
|
||||
<Tooltip
|
||||
v-if="section.infoTooltip"
|
||||
theme="dismissable-prompt"
|
||||
placement="top"
|
||||
:triggers="['hover', 'focus']"
|
||||
>
|
||||
<span
|
||||
class="inline-flex size-6 shrink-0 items-center justify-center text-secondary transition-colors group-hover:text-primary"
|
||||
@click.stop
|
||||
>
|
||||
<UnknownIcon class="size-5" />
|
||||
</span>
|
||||
<template #popper>
|
||||
<p class="m-0 max-w-96 text-wrap text-sm font-medium leading-tight">
|
||||
{{ section.infoTooltip }}
|
||||
</p>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="section.kind === 'saved'"
|
||||
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
|
||||
>
|
||||
<SkinLikeTextButton
|
||||
ref="addSkinButton"
|
||||
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
|
||||
dropzone
|
||||
:drag-active="isAddSkinButtonDragActive"
|
||||
@click="emit('add-skin')"
|
||||
@dragenter="emit('add-skin-dragenter', $event)"
|
||||
@dragover="emit('add-skin-dragover', $event)"
|
||||
@dragleave="emit('add-skin-dragleave', $event)"
|
||||
@drop="emit('add-skin-drop', $event)"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusIcon class="size-8" />
|
||||
</template>
|
||||
{{ formatMessage(messages.addSkinButton) }}
|
||||
<template #subtitle>{{ formatMessage(messages.dragAndDropSubtitle) }}</template>
|
||||
</SkinLikeTextButton>
|
||||
|
||||
<SkinButton
|
||||
v-for="skin in section.skins"
|
||||
:key="skinKey(skin, 'saved-skin')"
|
||||
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
|
||||
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
|
||||
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
|
||||
:selected="isSkinSelected(skin)"
|
||||
:active="isSkinActive(skin)"
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
:aria-label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
|
||||
>
|
||||
<SkinButton
|
||||
v-for="skin in section.skins"
|
||||
:key="skinKey(skin, section.key)"
|
||||
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
|
||||
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
|
||||
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
|
||||
:selected="isSkinSelected(skin)"
|
||||
:active="isSkinActive(skin)"
|
||||
:tooltip="skin.name"
|
||||
@select="emit('select', skin)"
|
||||
/>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -79,7 +79,7 @@ watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||
})
|
||||
})
|
||||
|
||||
populateJumpBackIn()
|
||||
await populateJumpBackIn()
|
||||
.catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
|
||||
@@ -244,7 +244,7 @@ const messages = defineMessages({
|
||||
</TagItem>
|
||||
<div
|
||||
v-if="world.type === 'singleplayer'"
|
||||
class="text-sm text-secondary flex items-center gap-1 font-semibold"
|
||||
class="text-sm text-secondary flex items-center gap-1 font-semibold flex-nowrap whitespace-nowrap"
|
||||
>
|
||||
<UserIcon
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -93,7 +93,7 @@ defineExpose({ show, hide })
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="hide()">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -106,7 +106,7 @@ const titleMessage = defineMessage({
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="hide()">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets'
|
||||
import type { CardAction } from '@modrinth/ui'
|
||||
import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { kill, list as listInstances } from '@/helpers/profile.js'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
|
||||
import { getServerAddress } from '@/store/install.js'
|
||||
|
||||
interface BrowseServerInstance {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
interface ContextMenuHandle {
|
||||
showMenu: (
|
||||
event: MouseEvent,
|
||||
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
options: { name: string }[],
|
||||
) => void
|
||||
}
|
||||
|
||||
interface ContextMenuOptionClick {
|
||||
option: 'open_link' | 'copy_link'
|
||||
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject
|
||||
}
|
||||
|
||||
export interface UseAppServerBrowseOptions {
|
||||
instance: Ref<BrowseServerInstance | null>
|
||||
isFromWorlds: ComputedRef<boolean>
|
||||
allInstalledIds: ComputedRef<Set<string>>
|
||||
newlyInstalled: Ref<string[]>
|
||||
installingServerProjects: Ref<string[]>
|
||||
playServerProject: (projectId: string) => Promise<void>
|
||||
showAddServerToInstanceModal: (serverName: string, serverAddress: string) => void
|
||||
handleError: (error: unknown) => void
|
||||
router: Router
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
addToInstance: {
|
||||
id: 'app.browse.add-to-instance',
|
||||
defaultMessage: 'Add to instance',
|
||||
},
|
||||
addToInstanceName: {
|
||||
id: 'app.browse.add-to-instance-name',
|
||||
defaultMessage: 'Add to {instanceName}',
|
||||
},
|
||||
added: {
|
||||
id: 'app.browse.added',
|
||||
defaultMessage: 'Added',
|
||||
},
|
||||
alreadyAdded: {
|
||||
id: 'app.browse.already-added',
|
||||
defaultMessage: 'Already added',
|
||||
},
|
||||
})
|
||||
|
||||
export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const debugLog = useDebugLogger('BrowseServer')
|
||||
const serverPings = shallowRef<Record<string, number | undefined>>({})
|
||||
const serverPingCache = new Map<string, number | undefined>()
|
||||
const pendingServerPings = new Map<string, Promise<number | undefined>>()
|
||||
const runningServerProjects = ref<Record<string, string>>({})
|
||||
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
|
||||
const contextMenuRef = ref<ContextMenuHandle | null>(null)
|
||||
let serverPingCacheActive = true
|
||||
let unlistenProcesses: (() => void) | null = null
|
||||
|
||||
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('checkServerRunningStates', { hitCount: hits.length })
|
||||
const packs = await listInstances().catch((error) => {
|
||||
options.handleError(error)
|
||||
return []
|
||||
})
|
||||
const newRunning: Record<string, string> = {}
|
||||
for (const hit of hits) {
|
||||
const inst = packs.find(
|
||||
(pack: GameInstance) => pack.linked_data?.project_id === hit.project_id,
|
||||
)
|
||||
if (inst) {
|
||||
const processes = await get_by_profile_path(inst.path).catch(() => [])
|
||||
if (Array.isArray(processes) && processes.length > 0) {
|
||||
newRunning[hit.project_id] = inst.path
|
||||
}
|
||||
}
|
||||
}
|
||||
debugLog('runningServerProjects updated', newRunning)
|
||||
runningServerProjects.value = newRunning
|
||||
}
|
||||
|
||||
async function handleStopServerProject(projectId: string) {
|
||||
debugLog('handleStopServerProject', projectId)
|
||||
const instancePath = runningServerProjects.value[projectId]
|
||||
if (!instancePath) return
|
||||
await kill(instancePath).catch(() => {})
|
||||
const { [projectId]: _, ...rest } = runningServerProjects.value
|
||||
runningServerProjects.value = rest
|
||||
}
|
||||
|
||||
async function handlePlayServerProject(projectId: string) {
|
||||
debugLog('handlePlayServerProject', projectId)
|
||||
await options.playServerProject(projectId)
|
||||
checkServerRunningStates(lastServerHits.value)
|
||||
}
|
||||
|
||||
async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearchProject) {
|
||||
debugLog('handleAddServerToInstance', { projectId: project.project_id, name: project.name })
|
||||
const address = getServerAddress(project.minecraft_java_server)
|
||||
if (!address) return
|
||||
|
||||
if (options.instance.value) {
|
||||
try {
|
||||
await add_server_to_profile(
|
||||
options.instance.value.path,
|
||||
project.name,
|
||||
address,
|
||||
'prompt',
|
||||
project.project_id,
|
||||
project.minecraft_java_server?.content?.kind,
|
||||
)
|
||||
options.newlyInstalled.value.push(project.project_id)
|
||||
} catch (error) {
|
||||
options.handleError(error)
|
||||
}
|
||||
} else {
|
||||
options.showAddServerToInstanceModal(project.name, address)
|
||||
}
|
||||
}
|
||||
|
||||
async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('pingServerHits', { hitCount: hits.length })
|
||||
const pingsToFetch = hits.flatMap((hit) => {
|
||||
const address = hit.minecraft_java_server?.address
|
||||
if (!address) return []
|
||||
return [{ hit, address }]
|
||||
})
|
||||
const nextPings = { ...serverPings.value }
|
||||
for (const { hit, address } of pingsToFetch) {
|
||||
if (serverPingCache.has(address)) {
|
||||
nextPings[hit.project_id] = serverPingCache.get(address)
|
||||
}
|
||||
}
|
||||
serverPings.value = nextPings
|
||||
|
||||
await Promise.all(
|
||||
pingsToFetch.map(async ({ hit, address }) => {
|
||||
if (serverPingCache.has(address)) return
|
||||
|
||||
let pending = pendingServerPings.get(address)
|
||||
if (!pending) {
|
||||
pending = getServerLatency(address)
|
||||
.then((latency) => {
|
||||
if (serverPingCacheActive) serverPingCache.set(address, latency)
|
||||
return latency
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Failed to ping server ${address}:`, error)
|
||||
if (serverPingCacheActive) serverPingCache.set(address, undefined)
|
||||
return undefined
|
||||
})
|
||||
.finally(() => {
|
||||
pendingServerPings.delete(address)
|
||||
})
|
||||
pendingServerPings.set(address, pending)
|
||||
}
|
||||
|
||||
const latency = await pending
|
||||
if (!serverPingCacheActive) return
|
||||
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function updateServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
lastServerHits.value = hits
|
||||
pingServerHits(hits)
|
||||
checkServerRunningStates(hits)
|
||||
}
|
||||
|
||||
function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject) {
|
||||
const content = project.minecraft_java_server?.content
|
||||
if (content?.kind === 'modpack') {
|
||||
const { project_name, project_icon, project_id } = content
|
||||
if (!project_name) return undefined
|
||||
return {
|
||||
name: project_name,
|
||||
icon: project_icon ?? undefined,
|
||||
onclick:
|
||||
project_id !== project.project_id
|
||||
? () => {
|
||||
options.router.push(`/project/${project_id}`)
|
||||
}
|
||||
: undefined,
|
||||
showCustomModpackTooltip: project_id === project.project_id,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getServerCardActions(
|
||||
serverResult: Labrinth.Search.v3.ResultSearchProject,
|
||||
): CardAction[] {
|
||||
const isInstalled = options.allInstalledIds.value.has(serverResult.project_id)
|
||||
|
||||
if (options.isFromWorlds.value && options.instance.value) {
|
||||
return [
|
||||
{
|
||||
key: 'add-to-instance',
|
||||
label: formatMessage(isInstalled ? messages.added : messages.addToInstance),
|
||||
icon: isInstalled ? CheckIcon : PlusIcon,
|
||||
disabled: isInstalled,
|
||||
color: 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => handleAddServerToInstance(serverResult),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const actions: CardAction[] = []
|
||||
|
||||
actions.push({
|
||||
key: 'add',
|
||||
label: '',
|
||||
icon: isInstalled ? CheckIcon : PlusIcon,
|
||||
disabled: isInstalled,
|
||||
circular: true,
|
||||
tooltip: isInstalled
|
||||
? formatMessage(messages.alreadyAdded)
|
||||
: options.instance.value
|
||||
? formatMessage(messages.addToInstanceName, {
|
||||
instanceName: options.instance.value.name,
|
||||
})
|
||||
: formatMessage(commonMessages.addServerToInstanceButton),
|
||||
onClick: () => handleAddServerToInstance(serverResult),
|
||||
})
|
||||
|
||||
if (runningServerProjects.value[serverResult.project_id]) {
|
||||
actions.push({
|
||||
key: 'stop',
|
||||
label: formatMessage(commonMessages.stopButton),
|
||||
icon: StopCircleIcon,
|
||||
color: 'red',
|
||||
type: 'outlined',
|
||||
onClick: () => handleStopServerProject(serverResult.project_id),
|
||||
})
|
||||
} else {
|
||||
const isInstalling = options.installingServerProjects.value.includes(serverResult.project_id)
|
||||
actions.push({
|
||||
key: 'play',
|
||||
label: formatMessage(
|
||||
isInstalling ? commonMessages.installingLabel : commonMessages.playButton,
|
||||
),
|
||||
icon: PlayIcon,
|
||||
disabled: isInstalling,
|
||||
color: 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => handlePlayServerProject(serverResult.project_id),
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
function handleRightClick(
|
||||
event: MouseEvent,
|
||||
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
) {
|
||||
contextMenuRef.value?.showMenu(event, result, [{ name: 'open_link' }, { name: 'copy_link' }])
|
||||
}
|
||||
|
||||
function handleOptionsClick(args: ContextMenuOptionClick) {
|
||||
const url = getProjectUrl(args.item)
|
||||
switch (args.option) {
|
||||
case 'open_link':
|
||||
openUrl(url)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(url)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
process_listener((event: { event: string; profile_path_id: string }) => {
|
||||
debugLog('process event', event)
|
||||
if (event.event === 'finished') {
|
||||
const projectId = Object.entries(runningServerProjects.value).find(
|
||||
([, path]) => path === event.profile_path_id,
|
||||
)?.[0]
|
||||
if (projectId) {
|
||||
const { [projectId]: _, ...rest } = runningServerProjects.value
|
||||
runningServerProjects.value = rest
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
unlistenProcesses = unlisten
|
||||
})
|
||||
.catch(options.handleError)
|
||||
|
||||
onUnmounted(() => {
|
||||
serverPingCacheActive = false
|
||||
unlistenProcesses?.()
|
||||
serverPingCache.clear()
|
||||
pendingServerPings.clear()
|
||||
})
|
||||
|
||||
return {
|
||||
serverPings,
|
||||
contextMenuRef,
|
||||
updateServerHits,
|
||||
getServerModpackContent,
|
||||
getServerCardActions,
|
||||
handleRightClick,
|
||||
handleOptionsClick,
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectUrl(
|
||||
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
) {
|
||||
const projectType = 'project_types' in item ? item.project_types?.[0] : item.project_type
|
||||
return `https://modrinth.com/${projectType ?? 'project'}/${item.slug ?? item.project_id}`
|
||||
}
|
||||
@@ -8,7 +8,7 @@ interface LogEntry {
|
||||
filename: string
|
||||
name?: string
|
||||
log_type: string
|
||||
stdout?: string
|
||||
output?: string | null
|
||||
age?: number
|
||||
live?: boolean
|
||||
}
|
||||
@@ -50,12 +50,12 @@ async function getHistoricalLogs(profilePathId: string, instancePath: string): P
|
||||
const entry = getOrCreate(profilePathId)
|
||||
if (entry.logList) return entry.logList
|
||||
|
||||
const logs: LogEntry[] = await get_logs(instancePath, false)
|
||||
const logs: LogEntry[] = await get_logs(instancePath, true)
|
||||
entry.logList = logs
|
||||
|
||||
for (const log of logs) {
|
||||
if (log.stdout && log.stdout !== '') {
|
||||
entry.historicalCache.set(log.filename, log.stdout)
|
||||
if (log.output) {
|
||||
entry.historicalCache.set(log.filename, log.output)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { test_jre } from '@/helpers/jre.js'
|
||||
|
||||
export default function useJavaTest() {
|
||||
const testingJava = ref(false)
|
||||
const javaTestResult = ref<boolean | null>(null)
|
||||
let testDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function runJavaTest(path: string, version: number, track = true) {
|
||||
if (testDebounceTimer) {
|
||||
clearTimeout(testDebounceTimer)
|
||||
testDebounceTimer = null
|
||||
}
|
||||
if (!path) {
|
||||
javaTestResult.value = null
|
||||
return
|
||||
}
|
||||
testingJava.value = true
|
||||
try {
|
||||
javaTestResult.value = await test_jre(path, version)
|
||||
} catch {
|
||||
javaTestResult.value = false
|
||||
}
|
||||
testingJava.value = false
|
||||
|
||||
if (track) {
|
||||
trackEvent('JavaTest', { path, success: javaTestResult.value })
|
||||
}
|
||||
}
|
||||
|
||||
function testJavaInstallationDebounced(path: string, version: number, delay = 600) {
|
||||
if (testDebounceTimer) clearTimeout(testDebounceTimer)
|
||||
if (!path) {
|
||||
javaTestResult.value = null
|
||||
return
|
||||
}
|
||||
testDebounceTimer = setTimeout(() => runJavaTest(path, version, false), delay)
|
||||
}
|
||||
|
||||
async function testJavaInstallation(path: string, version: number, track = false) {
|
||||
await runJavaTest(path, version, track)
|
||||
}
|
||||
|
||||
return {
|
||||
testingJava,
|
||||
javaTestResult,
|
||||
testJavaInstallationDebounced,
|
||||
testJavaInstallation,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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()
|
||||
},
|
||||
}
|
||||
@@ -98,6 +98,10 @@ export async function friend_listener(callback) {
|
||||
return await listen('friend', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function notification_listener(callback) {
|
||||
return await listen('notification', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'log' event
|
||||
/*
|
||||
LogPayload {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import type {
|
||||
ContentItem,
|
||||
ContentModpackCardCategory,
|
||||
ContentModpackCardProject,
|
||||
ContentModpackCardVersion,
|
||||
ContentOwner,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import {
|
||||
get_content_items,
|
||||
get_linked_modpack_info,
|
||||
type LinkedModpackInfo,
|
||||
} from '@/helpers/profile'
|
||||
import { get_categories } from '@/helpers/tags.js'
|
||||
import type { CacheBehaviour } from '@/helpers/types'
|
||||
|
||||
export type InstanceContentData = {
|
||||
path: string
|
||||
contentItems: ContentItem[] | null
|
||||
modpack: InstanceContentModpackData | null
|
||||
}
|
||||
|
||||
export type InstanceContentModpackData = {
|
||||
project: ContentModpackCardProject
|
||||
version: ContentModpackCardVersion
|
||||
owner: ContentOwner | null
|
||||
categories: ContentModpackCardCategory[]
|
||||
hasUpdate: boolean
|
||||
updateVersionId: string | null
|
||||
}
|
||||
|
||||
export async function loadInstanceContentData(
|
||||
path: string,
|
||||
cacheBehaviour?: CacheBehaviour,
|
||||
onError?: (error: Error) => unknown,
|
||||
): Promise<InstanceContentData> {
|
||||
const [contentItems, modpackInfo, allCategories] = await Promise.all([
|
||||
get_content_items(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
|
||||
get_linked_modpack_info(path, cacheBehaviour).catch((error) => handleLoadError(error, onError)),
|
||||
get_categories().catch((error) => handleLoadError(error, onError)),
|
||||
])
|
||||
|
||||
return {
|
||||
path,
|
||||
contentItems: (contentItems as ContentItem[] | null | undefined) ?? null,
|
||||
modpack: normalizeLinkedModpackInfo(
|
||||
modpackInfo as LinkedModpackInfo | null | undefined,
|
||||
allCategories as ContentModpackCardCategory[] | null | undefined,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoadError(error: unknown, onError?: (error: Error) => unknown) {
|
||||
onError?.(error as Error)
|
||||
return null
|
||||
}
|
||||
|
||||
function normalizeLinkedModpackInfo(
|
||||
modpackInfo: LinkedModpackInfo | null | undefined,
|
||||
allCategories: ContentModpackCardCategory[] | null | undefined,
|
||||
): InstanceContentModpackData | null {
|
||||
if (!modpackInfo) return null
|
||||
|
||||
return {
|
||||
project: {
|
||||
...modpackInfo.project,
|
||||
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
|
||||
icon_url: modpackInfo.project.icon_url ?? undefined,
|
||||
},
|
||||
version: {
|
||||
...modpackInfo.version,
|
||||
date_published: modpackInfo.version.date_published.toString(),
|
||||
},
|
||||
owner: modpackInfo.owner
|
||||
? {
|
||||
...modpackInfo.owner,
|
||||
avatar_url: modpackInfo.owner.avatar_url ?? undefined,
|
||||
}
|
||||
: null,
|
||||
categories: resolveLinkedModpackCategories(modpackInfo, allCategories),
|
||||
hasUpdate: modpackInfo.has_update,
|
||||
updateVersionId: modpackInfo.update_version_id,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLinkedModpackCategories(
|
||||
modpackInfo: LinkedModpackInfo,
|
||||
allCategories: ContentModpackCardCategory[] | null | undefined,
|
||||
) {
|
||||
if (!allCategories || !modpackInfo.project.categories) return []
|
||||
|
||||
const seen = new Set<string>()
|
||||
return allCategories.filter((category) => {
|
||||
if (modpackInfo.project.categories.includes(category.name) && !seen.has(category.name)) {
|
||||
seen.add(category.name)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
@@ -6,12 +6,11 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
/*
|
||||
A log is a struct containing the filename string, stdout, and stderr, as follows:
|
||||
A log is a struct containing the filename string and optional output, as follows:
|
||||
|
||||
pub struct Logs {
|
||||
pub filename: String,
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
pub output: Option<String>,
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +147,16 @@ export async function get_mod_full_path(path: string, projectPath: string): Prom
|
||||
return await invoke('plugin:profile|profile_get_mod_full_path', { path, projectPath })
|
||||
}
|
||||
|
||||
export interface JavaVersion {
|
||||
parsed_version: number
|
||||
version: string
|
||||
architecture: string
|
||||
path: string
|
||||
}
|
||||
|
||||
// Get optimal java version from profile
|
||||
// Returns a java version
|
||||
export async function get_optimal_jre_key(path: string): Promise<string | null> {
|
||||
export async function get_optimal_jre_key(path: string): Promise<JavaVersion | null> {
|
||||
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
|
||||
}
|
||||
|
||||
@@ -184,8 +191,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' | 'update'
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { LocationQuery, LocationQueryRaw } from 'vue-router'
|
||||
|
||||
const MODRINTH_HOSTNAMES = new Set(['modrinth.com', 'www.modrinth.com'])
|
||||
|
||||
const SUPPORTED_PROJECT_TYPES = new Set([
|
||||
'mod',
|
||||
'modpack',
|
||||
'resourcepack',
|
||||
'datapack',
|
||||
'plugin',
|
||||
'shader',
|
||||
'server',
|
||||
'project',
|
||||
])
|
||||
|
||||
export function parseModrinthLink(
|
||||
href: string,
|
||||
): { slug: string; pathSuffix: string; url: URL } | null {
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(href)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!MODRINTH_HOSTNAMES.has(url.hostname.toLowerCase())) {
|
||||
return null
|
||||
}
|
||||
|
||||
const segments = url.pathname.split('/').filter((p) => p.length > 0)
|
||||
if (segments.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (SUPPORTED_PROJECT_TYPES.has(segments[0].toLowerCase())) {
|
||||
const slug = segments[1]
|
||||
if (!slug) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rest: string[] = segments.slice(2)
|
||||
const pathSuffix = toValidAppSubpath(rest)
|
||||
if (pathSuffix === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { slug, pathSuffix, url }
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const SUPPORTED_SUBPATHS = ['versions', 'gallery']
|
||||
|
||||
function toValidAppSubpath(rest: string[]): string | null {
|
||||
if (rest.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const subroute = rest[0].toLowerCase()
|
||||
if (rest.length === 1 && SUPPORTED_SUBPATHS.includes(subroute)) {
|
||||
return `/${subroute}`
|
||||
}
|
||||
|
||||
if (rest.length === 2 && subroute === 'version') {
|
||||
return `/version/${rest[1]}`
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function mergeUrlQuery(routeQuery: LocationQuery, linkUrl: URL): LocationQueryRaw {
|
||||
const newQuery: LocationQueryRaw = { ...routeQuery }
|
||||
const keys = new Set<string>()
|
||||
linkUrl.searchParams.forEach((_value, key) => {
|
||||
keys.add(key)
|
||||
})
|
||||
for (const key of keys) {
|
||||
const values = linkUrl.searchParams.getAll(key)
|
||||
newQuery[key] = values.length === 1 ? values[0] : values
|
||||
}
|
||||
return newQuery
|
||||
}
|
||||
@@ -3,9 +3,8 @@ import {
|
||||
applyCapeTexture,
|
||||
createTransparentTexture,
|
||||
disposeCaches,
|
||||
loadTexture,
|
||||
setupSkinModel,
|
||||
} from '@modrinth/utils'
|
||||
} from '@modrinth/ui'
|
||||
import * as THREE from 'three'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
@@ -29,6 +28,7 @@ class BatchSkinRenderer {
|
||||
private scene: THREE.Scene | null = null
|
||||
private camera: THREE.PerspectiveCamera | null = null
|
||||
private currentModel: THREE.Group | null = null
|
||||
private transparentTexture: THREE.Texture | null = null
|
||||
private readonly width: number
|
||||
private readonly height: number
|
||||
|
||||
@@ -52,6 +52,7 @@ class BatchSkinRenderer {
|
||||
})
|
||||
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.renderer.shadowMap.enabled = false
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
this.renderer.toneMappingExposure = 10.0
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
@@ -62,7 +63,7 @@ class BatchSkinRenderer {
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.castShadow = false
|
||||
directionalLight.position.set(2, 4, 3)
|
||||
this.scene.add(ambientLight)
|
||||
this.scene.add(directionalLight)
|
||||
@@ -112,9 +113,19 @@ class BatchSkinRenderer {
|
||||
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
|
||||
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
||||
const response = await fetch(dataUrl)
|
||||
return await response.blob()
|
||||
return await new Promise<Blob>((resolve, reject) => {
|
||||
this.renderer!.domElement.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from rendered canvas'))
|
||||
}
|
||||
},
|
||||
'image/webp',
|
||||
0.9,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||
@@ -122,14 +133,10 @@ class BatchSkinRenderer {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl)
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl, capeUrl)
|
||||
|
||||
if (capeUrl) {
|
||||
const capeTexture = await loadTexture(capeUrl)
|
||||
applyCapeTexture(model, capeTexture)
|
||||
} else {
|
||||
const transparentTexture = createTransparentTexture()
|
||||
applyCapeTexture(model, null, transparentTexture)
|
||||
if (!capeUrl) {
|
||||
applyCapeTexture(model, null, this.getTransparentTexture())
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
@@ -141,39 +148,38 @@ class BatchSkinRenderer {
|
||||
this.currentModel = group
|
||||
}
|
||||
|
||||
private clearScene(): void {
|
||||
if (!this.scene) return
|
||||
|
||||
while (this.scene.children.length > 0) {
|
||||
const child = this.scene.children[0]
|
||||
this.scene.remove(child)
|
||||
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.geometry) child.geometry.dispose()
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((material) => material.dispose())
|
||||
} else {
|
||||
child.material.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
private getTransparentTexture(): THREE.Texture {
|
||||
if (!this.transparentTexture) {
|
||||
this.transparentTexture = createTransparentTexture()
|
||||
}
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.position.set(2, 4, 3)
|
||||
this.scene.add(ambientLight)
|
||||
this.scene.add(directionalLight)
|
||||
return this.transparentTexture
|
||||
}
|
||||
|
||||
private clearScene(): void {
|
||||
if (!this.scene || !this.currentModel) return
|
||||
|
||||
this.scene.remove(this.currentModel)
|
||||
this.currentModel.clear()
|
||||
this.currentModel = null
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clearScene()
|
||||
|
||||
if (this.transparentTexture) {
|
||||
this.transparentTexture.dispose()
|
||||
this.transparentTexture = null
|
||||
}
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose()
|
||||
}
|
||||
|
||||
this.renderer = null
|
||||
this.scene = null
|
||||
this.camera = null
|
||||
|
||||
disposeCaches()
|
||||
}
|
||||
}
|
||||
@@ -194,6 +200,9 @@ export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||
const DEBUG_MODE = false
|
||||
|
||||
let sharedRenderer: BatchSkinRenderer | null = null
|
||||
let latestPreviewGeneration = 0
|
||||
let previewGenerationQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
function getSharedRenderer(): BatchSkinRenderer {
|
||||
if (!sharedRenderer) {
|
||||
sharedRenderer = new BatchSkinRenderer()
|
||||
@@ -356,7 +365,27 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
||||
return await generateHeadRender(skin)
|
||||
}
|
||||
|
||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
export function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
const generation = ++latestPreviewGeneration
|
||||
const skinsSnapshot = [...skins]
|
||||
const capesSnapshot = [...capes]
|
||||
|
||||
const generationPromise = previewGenerationQueue.then(() =>
|
||||
generateSkinPreviewsForGeneration(skinsSnapshot, capesSnapshot, generation),
|
||||
)
|
||||
|
||||
previewGenerationQueue = generationPromise.catch(() => {})
|
||||
|
||||
return generationPromise
|
||||
}
|
||||
|
||||
async function generateSkinPreviewsForGeneration(
|
||||
skins: Skin[],
|
||||
capes: Cape[],
|
||||
generation: number,
|
||||
): Promise<void> {
|
||||
const isCurrentGeneration = () => generation === latestPreviewGeneration
|
||||
|
||||
try {
|
||||
const skinKeys = skins.map(
|
||||
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
||||
@@ -368,6 +397,8 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
headStorage.batchRetrieve(headKeys),
|
||||
])
|
||||
|
||||
if (!isCurrentGeneration()) return
|
||||
|
||||
for (let i = 0; i < skins.length; i++) {
|
||||
const skinKey = skinKeys[i]
|
||||
const headKey = headKeys[i]
|
||||
@@ -388,6 +419,8 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
}
|
||||
|
||||
for (const skin of skins) {
|
||||
if (!isCurrentGeneration()) return
|
||||
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
|
||||
if (skinBlobUrlMap.has(key)) {
|
||||
@@ -419,6 +452,8 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
cape?.texture,
|
||||
)
|
||||
|
||||
if (!isCurrentGeneration()) return
|
||||
|
||||
const renderResult: RenderResult = {
|
||||
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
||||
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
||||
@@ -439,9 +474,12 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
}
|
||||
} finally {
|
||||
disposeSharedRenderer()
|
||||
await cleanupUnusedPreviews(skins)
|
||||
|
||||
await skinPreviewStorage.debugCalculateStorage()
|
||||
await headStorage.debugCalculateStorage()
|
||||
if (isCurrentGeneration()) {
|
||||
await cleanupUnusedPreviews(skins)
|
||||
|
||||
await skinPreviewStorage.debugCalculateStorage()
|
||||
await headStorage.debugCalculateStorage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ export interface Cape {
|
||||
id: string
|
||||
name: string
|
||||
texture: string
|
||||
is_default: boolean
|
||||
is_equipped: boolean
|
||||
}
|
||||
|
||||
@@ -15,6 +14,7 @@ export type SkinSource = 'default' | 'custom_external' | 'custom'
|
||||
export interface Skin {
|
||||
texture_key: string
|
||||
name?: string
|
||||
section?: string
|
||||
variant: SkinModel
|
||||
cape_id?: string
|
||||
texture: string
|
||||
@@ -121,17 +121,11 @@ export async function get_available_skins(): Promise<Skin[]> {
|
||||
export async function add_and_equip_custom_skin(
|
||||
textureBlob: Uint8Array,
|
||||
variant: SkinModel,
|
||||
capeOverride?: Cape,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
|
||||
cape?: Cape,
|
||||
): Promise<Skin> {
|
||||
return await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
|
||||
textureBlob,
|
||||
variant,
|
||||
capeOverride,
|
||||
})
|
||||
}
|
||||
|
||||
export async function set_default_cape(cape?: Cape): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|set_default_cape', {
|
||||
cape,
|
||||
})
|
||||
}
|
||||
@@ -148,6 +142,22 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function save_custom_skin(
|
||||
skin: Skin,
|
||||
textureBlob: Uint8Array,
|
||||
variant: SkinModel,
|
||||
cape: Cape | undefined,
|
||||
replaceTexture: boolean,
|
||||
): Promise<Skin> {
|
||||
return await invoke('plugin:minecraft-skins|save_custom_skin', {
|
||||
skin,
|
||||
textureBlob,
|
||||
variant,
|
||||
cape,
|
||||
replaceTexture,
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
|
||||
const data = await normalize_skin_texture(skin.texture)
|
||||
const base64 = arrayBufferToBase64(data)
|
||||
@@ -162,6 +172,16 @@ export async function unequip_skin(): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|unequip_skin')
|
||||
}
|
||||
|
||||
export async function flush_pending_skin_change(): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|flush_pending_skin_change')
|
||||
}
|
||||
|
||||
export async function flush_pending_skin_change_for_profile(profileId: string): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|flush_pending_skin_change_for_profile', {
|
||||
profileId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
|
||||
const data = await invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
|
||||
return new Uint8Array(data)
|
||||
|
||||
@@ -5,15 +5,47 @@
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export interface LoadingBarType {
|
||||
type?: string
|
||||
version?: string
|
||||
profile_path?: string
|
||||
pack_name?: string
|
||||
icon?: string | null
|
||||
}
|
||||
|
||||
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 +53,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')
|
||||
}
|
||||
+3
@@ -14,6 +14,7 @@ export type GameInstance = {
|
||||
groups: string[]
|
||||
|
||||
linked_data?: LinkedData
|
||||
preferred_update_channel: ReleaseChannel
|
||||
|
||||
created: Date
|
||||
modified: Date
|
||||
@@ -46,6 +47,8 @@ type LinkedData = {
|
||||
locked: boolean
|
||||
}
|
||||
|
||||
type ReleaseChannel = 'release' | 'beta' | 'alpha'
|
||||
|
||||
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||
|
||||
type ContentFile = {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
const MIDAS_BITFLAG = 1 << 0
|
||||
const PRIDE_26_MIDAS_DURATION_MS = 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
type Pride26Campaign = Labrinth.Users.v3.Pride26CampaignDonation | null | undefined
|
||||
|
||||
export function hasMidasBadge(user?: { badges?: number } | null) {
|
||||
return !!user?.badges && (user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG
|
||||
}
|
||||
|
||||
export function hasPride26Badge(campaign: Pride26Campaign) {
|
||||
return campaign?.has_badge === true
|
||||
}
|
||||
|
||||
export function hasActivePride26Midas(campaign: Pride26Campaign, now = Date.now()) {
|
||||
if (campaign?.has_midas !== true) {
|
||||
return false
|
||||
}
|
||||
|
||||
const donatedAt = new Date(campaign.last_donated_at).getTime()
|
||||
return Number.isFinite(donatedAt) && donatedAt + PRIDE_26_MIDAS_DURATION_MS > now
|
||||
}
|
||||
@@ -22,6 +22,10 @@ export async function removeEnqueuedUpdate() {
|
||||
return await invoke('remove_enqueued_update')
|
||||
}
|
||||
|
||||
export async function setRestartAfterPendingUpdate(should_restart) {
|
||||
return await invoke('set_restart_after_pending_update', { shouldRestart: should_restart })
|
||||
}
|
||||
|
||||
// One of 'Windows', 'Linux', 'MacOS'
|
||||
export async function getOS() {
|
||||
return await invoke('plugin:utils|get_os')
|
||||
|
||||
@@ -1,10 +1,73 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "جاز تنزيل إصدار جافا {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "التنزيلات"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "إخفاء المزيد من الحالات قيد التشغيل"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "إنشاء نسخة أساسية"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "لا توجد نسخ قيد التشغيل"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "غير متصل"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "النسخة ألرئيسية"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "عرض المزيد من النسخ قيد التشغيل"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "إيقاف النسخة"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "عرض التحميلات الفعالة"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "عرض النسخة"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "عرض التسجيل"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "صفحة الرئيسية"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "مكتبة"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "تعذر الوصول إلى خوادم المصادقة"
|
||||
},
|
||||
"app.browse.add-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.server.installing": {
|
||||
"message": "جاري التثبيت"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "أدخل وصف التعديل..."
|
||||
},
|
||||
@@ -20,9 +83,6 @@
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "إسم حزمة التعديل"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "حدد الملفات والمجلدات المراد تضمينها في الحزمة"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "رقم الإصدار"
|
||||
},
|
||||
@@ -30,19 +90,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 +120,7 @@
|
||||
"message": "تمت إضافة \"{name}\""
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "تمت إضافة {count} من المشاريع"
|
||||
"message": "تمت إضافة {count} مشروع"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "تحقق من المشاريع التي أستخدمها في حزمة التعديل الخاص بي!"
|
||||
@@ -62,6 +131,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 +195,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 +210,7 @@
|
||||
"message": "يلزم التحديث"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "هناك تحديث لازم للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
|
||||
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "تم تفعيل وضع المطوّر."
|
||||
@@ -122,9 +236,6 @@
|
||||
"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."
|
||||
},
|
||||
@@ -155,14 +266,23 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "تم تثبيت الإصدار {version} بنجاح!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "تنزيل التحديث"
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "مثال.مودرنث.جج"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "جار تنزيل التحديث ({percent}٪)"
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "حدد خيارا"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"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 +383,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 +426,7 @@
|
||||
"message": "نسخة النسخة"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-instance.description": {
|
||||
"message": "إنشاء."
|
||||
"message": "ينشئ نسخة من هذا النموذج شاملا عوالمه, تعديلاتها النصيه, إلخ..."
|
||||
},
|
||||
"instance.settings.tabs.general.edit-icon": {
|
||||
"message": "تعديل الأيقونة"
|
||||
@@ -464,6 +590,15 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "العالم قيد الاستخدام"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "إضافة حساب"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "اختيار الحساب"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "سجل دخولك لـ ماينكرافت"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "مقدَّم من النسخة"
|
||||
},
|
||||
@@ -480,12 +615,15 @@
|
||||
"message": "يقدمها الخادم"
|
||||
},
|
||||
"search.filter.locked.server-environment.title": {
|
||||
"message": "يمكن إضافة التعديلات من جانب العميل فقط إلى مثيل الخادم"
|
||||
"message": "يمكن إضافة التعديلات **المحليه** فقط إلى نموذج الخادم"
|
||||
},
|
||||
"search.filter.locked.server-game-version.title": {
|
||||
"message": "يتم توفير نسخة اللعبة من قبل الخادم"
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "يتم توفير المحمّل من قبل الخادم"
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "لا تعرض هذا التحذير مجددا"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,268 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Stahování Javy {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Stažené Soubory"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Skrýt více spuštěných instancí"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Použít jako primární instanci"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Žádné spuštěné instance"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Primární instance"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Zobrazit další spuštěné instance"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Ukončit instanci"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Zobrazit probíhající stahování"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Zobrazit instanci"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Zobrazit záznamy"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Umožňuje pokročilé vykreslování, jako jsou efekty rozostření, které mohou způsobit problémy s výkonem bez hardwarového vykreslení."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Pokročilé vykreslování"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Vyberte si váš preferovaný motiv pro aplikaci Modrinth."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Barevný motiv"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Změňte stránku, na které se spouštěč otevře."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Domov"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Knihovna"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Výchozí úvodní stránka"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Zakáže zobrazení jmenovky nad vaší postavou na stránce skinů."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Skrýt jmenovku"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "V sekci „Vrátit se do hry“ na úvodní stránce jsou uvedeny nejnovější světy."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Skočit zpět do světů"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimalizovat spouštěč při spuštění procesu Minecraftu."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimalizovat spouštěč"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Použít systémový rámeček okna (vyžaduje restart aplikace)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Nativní dekorace"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Vyberte možnost"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Ukázat herní čas"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Umožňí přepínat zobrazení postranního panelu."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Přepnout postranní panel"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Pokud se pokusíte nainstalovat soubor Modrinth Pack (.mrpack), který není hostován na Modrinth, zajistíme, abyste si před instalací uvědomili související rizika."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Upozorni mě před instalací neznámých modpacků"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Autořizační servery Minecraftu aktuálně nejsou k dispozici. Zkontrolujte si své připojení k internetu a zkuste to znovu později."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Připojení k autorizačním serverům se nezdařilo"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Přidat server do instance"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Přidat do instalace"
|
||||
},
|
||||
"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.back-to-instance": {
|
||||
"message": "Zpět k instanci"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Prozkoumat obsah"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Prozkoumat servery"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Skrýt již přidané servery"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Balíčky Modifikací"
|
||||
},
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Přidání obsahu může narušit kompatibilitu při připojení k serveru. Veškerý přidaný obsah se navíc ztratí při aktualizaci obsahu instance serveru."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Instalování"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Probíhá instalace modpacku..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Přidej popis modpacku..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Exportovat"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportovat modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Zahrnout \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Jméno Modpacku"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Jméno modpacku"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Číslo verze"
|
||||
},
|
||||
"app.export-modal.version-number-placeholder": {
|
||||
"message": "1.0.0"
|
||||
},
|
||||
"app.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-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-with-address": {
|
||||
"message": "'{name}' ({address}) bude odstraněn z vašeho seznamu, a to i ve hře, a nebude možné jej obnovit."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Opravdu chceš odstranit {name}?"
|
||||
},
|
||||
"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.content-required": {
|
||||
"message": "Požadovaný obsah"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Nainstaluj ke hraní"
|
||||
},
|
||||
@@ -20,12 +272,21 @@
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural,one {#mód} other {#módy}}"
|
||||
},
|
||||
"app.modal.install-to-play.required-modpack": {
|
||||
"message": "Povinný modpack"
|
||||
},
|
||||
"app.modal.install-to-play.server-requires-mods": {
|
||||
"message": "Tento server k hraní vyžaduje módy. Klikni na instalovat pro získání potřebných módů z Modrinth."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Sdílená instance"
|
||||
},
|
||||
"app.modal.install-to-play.shared-server-instance": {
|
||||
"message": "Sdílená serverová instance"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "Zobrazit obsah"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Updatuj ke hraní"
|
||||
},
|
||||
@@ -35,6 +296,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Je nutná aktualizace ke hraní {name}. Prosím updatuj na nejnovější verzi k spuštění hry."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Tento projekt je již nainstalován"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Zpět na Prozkoumat"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Nainstalovat obsah do instnce"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Vývojářský režim povolen."
|
||||
},
|
||||
@@ -59,29 +329,89 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Správa zdrojů"
|
||||
},
|
||||
"app.skins.add-button": {
|
||||
"message": "Přidat skin"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Použít"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Smazat skin"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Upravit skin"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Přidat skin"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Upravit skin"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Uložené skiny"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Nainstalujte aktualizaci nyní nebo automaticky po zavření aplikace Modrinth."
|
||||
},
|
||||
"app.update-popup.body.linux": {
|
||||
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
|
||||
},
|
||||
"app.update-popup.body.metered": {
|
||||
"message": "Aplikace Modrinth v{version} je nyní k dispozici! Protože jste v měřené síti, nebyla stažena automaticky."
|
||||
},
|
||||
"app.update-popup.changelog": {
|
||||
"message": "Seznam změn"
|
||||
},
|
||||
"app.update-popup.download": {
|
||||
"message": "Stáhnout ({size})"
|
||||
},
|
||||
"app.update-popup.download-complete": {
|
||||
"message": "Stahování bylo dokončeno"
|
||||
},
|
||||
"app.update-popup.reload": {
|
||||
"message": "Načíst znovu"
|
||||
},
|
||||
"app.update-popup.title": {
|
||||
"message": "Je k dispozici aktualizace"
|
||||
},
|
||||
"app.update.complete-toast.text": {
|
||||
"message": "Kliknutím sem zobrazíte seznam změn."
|
||||
},
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Verze {version} byla úspěšně nainstalována!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Stáhnout aktualizaci"
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "priklad.modrinth.gg"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Stahování aktualizace ({percent}%)"
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Vyberte možnost"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Restartovat aplikaci pro nainstalování aktualizace"
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "Nekompatibilní verze {version}"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "Ještě nehráno"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} online"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Přidat přítele"
|
||||
},
|
||||
"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í"
|
||||
@@ -128,9 +458,6 @@
|
||||
"friends.heading.pending": {
|
||||
"message": "Čeká na vyřízení"
|
||||
},
|
||||
"friends.no-friends-match": {
|
||||
"message": "Žádní přátelé neodpovídají '{query}''"
|
||||
},
|
||||
"friends.search-friends-placeholder": {
|
||||
"message": "Hledat přátele..."
|
||||
},
|
||||
@@ -176,6 +503,12 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "Upravit svět"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "Přidávání souborů ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "Uložit jako..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Adresa"
|
||||
},
|
||||
@@ -356,6 +689,9 @@
|
||||
"instance.worlds.incompatible_server": {
|
||||
"message": "Server je nekompatibilní"
|
||||
},
|
||||
"instance.worlds.linked_server": {
|
||||
"message": "Spravováno projektem serveru"
|
||||
},
|
||||
"instance.worlds.no_contact": {
|
||||
"message": "Server se nepodařilo spojit"
|
||||
},
|
||||
@@ -363,7 +699,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"
|
||||
@@ -374,6 +710,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Svět je použit"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Přidat účet"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Účet Minecraft"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Nejste přihlášeni"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Odstranit účet"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Vyberte účet"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Přihlásit se do Minecraftu"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Poskytováno instancí"
|
||||
},
|
||||
@@ -386,6 +740,9 @@
|
||||
"search.filter.locked.instance.sync": {
|
||||
"message": "Synchronizováno z instancí"
|
||||
},
|
||||
"search.filter.locked.server": {
|
||||
"message": "Poskytováno serverem"
|
||||
},
|
||||
"search.filter.locked.server-environment.title": {
|
||||
"message": "Jen klientské mody mohou být přidány na server"
|
||||
},
|
||||
@@ -394,5 +751,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader zprostředkovává server"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Soubor je zkontrolován pouze v případě, že je nahrán na Modrinth, a to bez ohledu na jeho formát (včetně formátu .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Toto varování již nezobrazovat"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Potvrdit instalaci"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Přesto nainstalovat"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Malware se často šíří prostřednictvím souborů s modpacky, které se sdílejí na platformách jako Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Tento soubor jsme na Modrinthu nenašli. Důrazně doporučujeme instalovat pouze soubory z důvěryhodných zdrojů."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Upozornění na neznámý soubor"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,154 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Downloader Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Gem flere kørende instances"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Gør til primære instance"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Ingen instances kørende"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Primære instance"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Vis flere kørende instances"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Stop instance"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Vis aktive downloads"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Vis instance"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Vis logs"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Aktiverer avanceret gengivelse som sløringseffekter, der kan forårsage ydelsesproblemer uden hardwareaccelereret gengivelse."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Avanceret rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Vælg dit foretrukne farvetema til Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Farvetema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Skift hvilken side launcheren åbner i."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Hjem"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Bibliotek"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Standard startside"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Slår nametagget over din spiller på skins siden fra."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Gem nametag"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Inkluderer de sidste verdener i afsnittet \"Hop tilbage\" på startsiden."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Hop tilbage ind i verdener"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimer launcheren når Minecraft starter."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimer launcher"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Vælg en mulighed"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Vis hvor meget tid du har brugt på at spille en instance."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Vis tid spillet"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Aktivere muligheden for at slå sidepanelet til og fra."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Slå sidepanel til og fra"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Hvis du prøver at installere en Modrinth Pack fil (.mrpack) som ikke er hosted på Modrinth, så skal vi nok sikre os du forstår riskoen før du installere den."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Advar mig før jeg installere en ukendt modpacks"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft authentication servere kan måske være nede lige nu. Tjek din internet forbindelse og prøv igen senere."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Kan ikke nå autentificeringsservere"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Tilføjet server til instance"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Tilføj til en instance"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Tilføj til instance"
|
||||
},
|
||||
"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.back-to-instance": {
|
||||
"message": "Tilbage til instance"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Opdag indhold"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Opdag servere"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Gem allerede tilføjet til servere"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Ved at tilføje indhold kan der opstå kompatibilitetsproblemer, når du tilslutter dig serveren. Alt tilføjet indhold går desuden tabt, når du opdaterer indholdet på din server instance."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installere"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installere modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Indtast modpack beskrivelse..."
|
||||
},
|
||||
@@ -14,6 +158,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Eksporter modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inkludere \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Navn"
|
||||
},
|
||||
@@ -21,7 +168,7 @@
|
||||
"message": "Modpack navn"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Vælg filer og mapper til at inkludere i pakken"
|
||||
"message": "Konfigurer hvilke filer inkludere i denne eksport"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
@@ -41,6 +188,18 @@
|
||||
"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.modpack-already-installed.header": {
|
||||
"message": "Modpack allerede installeret"
|
||||
},
|
||||
"app.instance.modpack-already-installed.instance": {
|
||||
"message": "Instance"
|
||||
},
|
||||
"app.instance.mods.content-type-project": {
|
||||
"message": "projekt"
|
||||
},
|
||||
@@ -59,6 +218,42 @@
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Uploadet"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Tilføj server"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Søg servere"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Er du sikker på din vil slette denne verden permanent?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Moddet"
|
||||
},
|
||||
"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": "Tilføj en server, eller søg efter en for at komme i gang"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Ingen servere eller verdener tilføjet"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Er du sikker på din vil fjerne {name}?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Søg {count} verdener..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "denne server"
|
||||
},
|
||||
"app.modal.install-to-play.content-required": {
|
||||
"message": "Indhold krævet"
|
||||
},
|
||||
@@ -95,6 +290,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Dette projekt er allerede installeret"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Tilbage til udforsk"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Installer indhold til instance"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Udvikler-tilstand aktiveret."
|
||||
},
|
||||
@@ -119,8 +323,137 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Ressourcestyring"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App v{version} er klar til at blive installeret! Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
|
||||
"app.skins.add-button": {
|
||||
"message": "Tilføj skin"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Træk og slip"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Brug"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Slet skin"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Dette vil permanent slette det valgte skin. Dette vil ikke kunne blive fortydes."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "Er du sikker på, at du vil slette dette skin?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Fejl ved læsning af den afleverede fil."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Fejl under behandling af fil"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Rediger skin"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Tilføj skin"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Tilføjer et skin"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Slank"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Bred"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Kappe"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Kappe"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Rediger skin"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "Rediger skinnet først!"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Ingen kappe"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Ingen"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Gem skin"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Gemmer..."
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "Upload et skin først!"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Rediger skin"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Forhåndsvisning"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "Du skifter dit skin for ofte. Mojang's servere har midlertidigt blokeret fremtidige anmodninger. Vent venligst et øjeblik for du prøver igen."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "Ro på!"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Standard skins"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "Du modtog disse skins for at donere til en Modrinth Pride indsamling imens der er Pride Month."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Mounts of Mayhem"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Gemte skins"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "The Copper Age"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "The Garden Awakens"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Tiny Takeover"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Log Ind"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Venligst log ind til din Minecraft konto for at kunne funktionerne til at administrere dit skin i Modrinth appen."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Overrasket Modrinth Bot"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Venligst log ind"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Skin vælger"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} er færdig med at download. Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
|
||||
@@ -152,14 +485,23 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Version {version} var installeret med succes!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Download opdatering"
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "eksemple.modrinth.gg"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Downloader opdatering ({percent}%)"
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Vælg en mulighed"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Genindlæs for at installere opdatering"
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "Ikke kompatibel version {version}"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "Ikke spillet endnu"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} online"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Tilføj en ven"
|
||||
@@ -260,6 +602,12 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "Rediger verden"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "Tilføjer filer ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "Gem som..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Adresse"
|
||||
},
|
||||
@@ -374,6 +722,9 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java og hukommelse"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Indskriv Java argumenter..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Omgivelses variabler"
|
||||
},
|
||||
@@ -389,6 +740,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Allokeret hukommelse"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/vej/til/java"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Vindue"
|
||||
},
|
||||
@@ -461,6 +815,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Verden er i brug"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Tilføj konto"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft konto"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Ikke logged ind"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Fjern konto"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Vælg konto"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Log ind på Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Givet af instanceen"
|
||||
},
|
||||
@@ -484,5 +856,20 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader er givet af serveren"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "En fil behandles kun, hvis den uploades til Modrinth, uanset filformat (herunder .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Vis ikke denne advarsel igen"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Bekræft installation"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Installer alligevel"
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Ukendt fil advarsel"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,120 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java {version} wird heruntergeladen"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen ausblenden"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Zur primären Instanz machen"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Keine Instanzen laufen"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Primäre Instanz"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen anzeigen"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Instanz stoppen"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Aktive Downloads anzeigen"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Instanz anzeigen"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Protokolle anzeigen"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschärfeeffekte, welche ohne hardwarebeschleunigtes Rendering zu Leistungsproblemen führen können."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Erweitertes Rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Wähle dein bevorzugtes Farbschema für die Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Farbschema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Start"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Bibliothek"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Standard-Startseite"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Deaktiviert das Namensschild über deinem Spieler auf der Skin-Seite."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Namensschild ausblenden"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Enthält zuletzt gespielte Welten im Abschnitt „Direkt weiterspielen“ auf der Startseite."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Springe zurück in Welten"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Den Launcher minimieren, wenn ein Minecraft-Prozess gestartet wird."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Launcher minimieren"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Systemfensterrahmen verwenden (Neustart der App erforderlich)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Native Dekorationen"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Option wählen"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Zeigt an, wie viel Zeit du mit dem Spielen einer Instanz verbracht hast."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Spielzeit anzeigen"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Ermöglicht das Umschalten der Seitenleiste."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Seitenleiste umschalten"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Falls du versuchst, eine Modrinth-Pack-Datei (.mrpack) zu installieren, die nicht auf Modrinth gehostet wird, werden wir sicherstellen, dass du die Risiken vor der Installation verstehst."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Warne mich, bevor unbekannte Modpacks installiert werden"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Die Authentifizierungsserver von Minecraft sind eventuell momentan nicht erreichbar. Überprüfe deine Internetverbindung und versuche es später erneut."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Authentifizierungsserver sind nicht erreichbar"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Server zu Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Server zu deiner Instanz hinzufügen"
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Zu Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Zu Instanz hinzufügen"
|
||||
@@ -23,6 +128,9 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "Bereits hinzugefügt"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Zu Instanz zurückgehen"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Inhalte entdecken"
|
||||
},
|
||||
@@ -30,13 +138,25 @@
|
||||
"message": "Server entdecken"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hinzugefügte Server ausblenden"
|
||||
"message": "Bereits hinzugefügte Server ausblenden"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Installierte Inhalte ausblenden"
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Inhalt in Instanz installieren"
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Das hinzufügen von Inhalten kann Kompatibilitätsprobleme beim beitreten eines Servers verursachen. Sämtliche hinzugefügte Inhalten gehen ausserdem beim aktualisieren der Server Instanz-Inhalte verloren."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Wird installiert"
|
||||
},
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Modpack wird installiert..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Modpaketbeschreibung eingeben..."
|
||||
@@ -47,6 +167,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modpack exportieren"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" einschliessen?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpaketname"
|
||||
},
|
||||
@@ -54,7 +177,7 @@
|
||||
"message": "Modpaketname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Wähle Dateien und Ordner zum hinzufügen im Paket aus"
|
||||
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
@@ -110,9 +233,6 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Server durchsuchen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "„{name}“ wird **endgültig gelöscht**, und es gibt keine Möglichkeit, sie wiederherzustellen."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
|
||||
},
|
||||
@@ -134,12 +254,6 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Keine Server oder Welten hinzugefügt"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "„{name}“ wird aus deiner Liste entfernt, auch im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "„{name}“ ({address}) wird aus deiner Liste entfernt, inklusive im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Möchtest du {name} wirklich entfernen?"
|
||||
},
|
||||
@@ -185,6 +299,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Eine aktualisierung zum spielen von {name} ist benötigt. Bitte aktualisiere auf die neuste Version um das Spiel zu starten."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Dieses Projekt ist bereits installiert"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Zurück zu Entdecken"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Inhalt in Instanz installieren"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Entwicklermodus aktiviert."
|
||||
},
|
||||
@@ -209,8 +332,146 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Ressourcenmanagement"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App v{version} ist bereit zur Installation! Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
|
||||
"app.skins.add-button": {
|
||||
"message": "Skin hinzufügen"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Drag-and-Drop"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Anwenden"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Skin löschen"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Dies wird den ausgewählten Skin unwiderruflich löschen. Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "Bist du dir sicher, dass du diesen Skin löschen willst?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Fehler beim Lesen der abgelegten Datei."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Fehler bei der Dateiverarbeitung"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Skin bearbeiten"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Skin hinzufügen"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Skin wird hinzugefügt"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Arm Stil"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Schlank"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Breit"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Umhang"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Umhang"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Skin bearbeiten"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "Nimm zuerst eine Änderung am Skin vor!"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Kein Umhang"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Keines"
|
||||
},
|
||||
"app.skins.modal.replace-texture-button": {
|
||||
"message": "Textur ersetzen"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Skin speichern"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Speichern..."
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Textur"
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "Lade zuerst einen Skin hoch!"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Skin bearbeiten"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Vorschau"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "Du änderst deinen Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "Nicht so schnell!"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Standard-Skins"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "Du hast diese Skins als Dankeschön für eine Spende bei der Modrinth Pride Spendenaktion während des Pride Month erhalten."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Mounts of Mayhem"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Gespeicherte Skins"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "The Copper Age"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "The Garden Awakens"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Tiny Takeover"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Anmelden"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Bitte melde dich in deinem Minecraft-Konto an, um die Skin-Verwaltungsfunktionen der Modrinth App zu nutzen."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Begeisterter Modrinth-Bot"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Bitte anmelden"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Skin-Auswahl"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} wurde heruntergeladen. Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
|
||||
@@ -242,15 +503,6 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Version {version} wurde erfolgreich installiert!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Aktualisierung herunterladen"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Lade Aktualisierung herunter ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Lade neu um Aktualisierung zu installieren"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
@@ -488,6 +740,24 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java und Arbeitsspeicher"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-environment-variables": {
|
||||
"message": "Benutzerdefinierte Umgebungsvariablen"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-arguments": {
|
||||
"message": "Benutzerdefinierte Java-Argumente"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-installation": {
|
||||
"message": "Benutzerdefinierte Java-Installation"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-memory-allocation": {
|
||||
"message": "Benutzerdefinierte Speicherzuweisung"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Umgebungsvariablen eingeben..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Java-Argumente eingeben..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Umgebungsvariablen"
|
||||
},
|
||||
@@ -503,6 +773,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Zugewiesener Arbeitsspeicher"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/pfad/zu/java"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Spielfenster"
|
||||
},
|
||||
@@ -575,6 +848,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Welt bereits in Benutzung"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Konto hinzufügen"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft-Konto"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Nicht angemeldet"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Konto entfernen"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Konto auswählen"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "In Minecraft anmelden"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Von der Instanz vorgegeben"
|
||||
},
|
||||
@@ -598,5 +889,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader wird vom Server bereitgestellt"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth hochgeladen wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Diese Warnung nicht mehr anzeigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Installation bestätigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Trotzdem installieren"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Schadsoftware wird häufig über Modpack-Dateien verbreitet, indem diese auf Plattformen wie Discord geteilt werden."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Wir konnten diese Datei auf Modrinth nicht finden. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Warnung vor unbekannter Datei"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,123 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java {version} wird heruntergeladen"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen ausblenden"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Zur primären Instanz machen"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Keine aktiven Instanzen"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Primäre Instanz"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen anzeigen"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Instanz stoppen"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Aktive Downloads anzeigen"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Instanz anzeigen"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Protokolle anzeigen"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschärfe-Effekte, welche ohne Hardware-beschleunigtes Rendering zu Leistungsproblemen führen können."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Erweitertes Rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Wähle dein bevorzugtes Farbschema für die Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Farbschema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Start"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Bibliothek"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Standard-Startseite"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Deaktiviert das Namensschild über deinem Spieler auf der Skin-Seite."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Namensschild ausblenden"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Enthält zuletzt gespielte Welten im Abschnitt „Direkt weiterspielen“ auf der Startseite."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Direkt in Welten weiterspielen"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Den Launcher minimieren, wenn ein Minecraft-Prozess gestartet wird."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Launcher minimieren"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Fensterdekorationen des Systems verwenden (Neustart der App erforderlich)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Native Dekorationen"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Wähle eine Option"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Zeigt an, wie viel Zeit du damit verbracht hast, eine Instanz zu spielen."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Spielzeit anzeigen"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Ermöglicht das Umschalten der Seitenleiste."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Seitenleiste umschalten"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Falls du versuchst, eine Modrinth-Pack-Datei (.mrpack) zu installieren, die nicht auf Modrinth gehostet wird, werden die Risiken vor der Installation bekannt gegeben."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Warne mich, bevor unbekannte Modpacks installiert werden"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Die Authentifizierungsserver von Minecraft sind eventuell momentan nicht erreichbar. Überprüfe deine Internetverbindung und versuche es später erneut."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Authentifizierungsserver sind nicht erreichbar"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Server zu Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Server zu deiner Instanz hinzufügen"
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Zu Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Zu Instanz hinzufügen"
|
||||
"message": "Zur Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Zu {instanceName} hinzufügen"
|
||||
@@ -23,6 +128,9 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "Bereits hinzugefügt"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Zurück zur Instanz"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Inhalte entdecken"
|
||||
},
|
||||
@@ -30,13 +138,25 @@
|
||||
"message": "Server entdecken"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hinzugefügte Server ausblenden"
|
||||
"message": "Bereits hinzugefügte Server ausblenden"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Installierte Inhalte ausblenden"
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Inhalt in Instanz installieren"
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Das Hinzufügen von Inhalten kann die Kompatibilität beim Beitreten zum Server beeinträchtigen. Außerdem gehen alle hinzugefügten Inhalte verloren, wenn du die Inhalte der Serverinstanz aktualisierst."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Wird installiert"
|
||||
},
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Keine verfügbaren Versionen stimmen mit {compatibilityLabel} überein. Wähle eine Version aus, um sie trotzdem zu installieren. Abhängigkeiten werden nicht automatisch installiert."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Modpack wird installiert..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Beschreibung des Modpacks eingeben..."
|
||||
@@ -47,6 +167,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Modpack exportieren"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "\"{file}\" einschließen?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpackname"
|
||||
},
|
||||
@@ -54,7 +177,7 @@
|
||||
"message": "Modpackname"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Wähle Dateien und Ordner aus, die in das Paket sollen"
|
||||
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versionsnummer"
|
||||
@@ -63,7 +186,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"
|
||||
@@ -110,9 +233,6 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Server durchsuchen"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "„{name}“ wird **endgültig gelöscht**, und es gibt keine Möglichkeit, sie wiederherzustellen."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Möchtest du diese Welt wirklich endgültig löschen?"
|
||||
},
|
||||
@@ -134,12 +254,6 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Keine Server oder Welten hinzugefügt"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "„{name}“ wird aus deiner Liste entfernt, auch im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "„{name}“ ({address}) wird aus deiner Liste entfernt, inklusive im Spiel, und es gibt keine Möglichkeit, ihn wiederherzustellen."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Möchtest du {name} wirklich entfernen?"
|
||||
},
|
||||
@@ -185,6 +299,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Zum Spielen von {name} ist eine Aktualisierung erforderlich. Bitte aktualisiere auf die neueste Version, um das Spiel zu starten."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Dieses Projekt ist bereits installiert"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Zurück zum Entdecken"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Inhalt in Instanz installieren"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Entwicklermodus aktiviert."
|
||||
},
|
||||
@@ -209,8 +332,146 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Ressourcenmanagement"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App v{version} ist bereit zur Installation! Neu laden, um jetzt zu aktualisieren, oder automatisch, wenn du die Modrinth App schließt."
|
||||
"app.skins.add-button": {
|
||||
"message": "Skin hinzufügen"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Ziehen und Ablegen"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Anwenden"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Skin löschen"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Dadurch wird der ausgewählte Skin permanent gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "Möchtest du diesen Skin wirklich löschen?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Die abgelegte Datei konnte nicht gelesen werden."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Fehler bei der Verarbeitung der Datei"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Skin bearbeiten"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Skin hinzufügen"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Einen Skin hinzufügen"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Arm-Stil"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Schlank"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Breit"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Umhang"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Umhang"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Skin bearbeiten"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "Nimm zuerst eine Änderung am Skin vor!"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Kein Umhang"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Keinen"
|
||||
},
|
||||
"app.skins.modal.replace-texture-button": {
|
||||
"message": "Textur ersetzen"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Skin speichern"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Wird gespeichert..."
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Textur"
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "Lade zuerst ein Skin hoch!"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Skin bearbeiten"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Vorschau"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "Du wechselst dein Skin zu oft. Die Server von Mojang haben weitere Anfragen vorübergehend blockiert. Bitte warte einen Moment, bevor du es erneut versuchst."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "Langsamer!"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Standard-Skins"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "Du hast diese Skins erhalten, da du während des Pride-Monats für eine Modrinth-Pride-Spendenaktion gespendet hast."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Mounts of Mayhem"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Gespeicherte Skins"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "The Copper Age"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "The Garden Awakens"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Tiny Takeover"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Anmelden"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Bitte melde dich bei deinem Minecraft-Konto an, um die Funktionen zur Skin-Verwaltung der Modrinth-App nutzen zu können."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Begeisterter Modrinth-Bot"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Bitte melde dich an"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Skin-Auswahl"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} wurde heruntergeladen. Neu laden, um jetzt zu aktualisieren, oder automatisch aktualisieren, wenn du die Modrinth App schließt."
|
||||
@@ -242,15 +503,6 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Version {version} wurde erfolgreich installiert!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Update herunterladen"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Update wird heruntergeladen ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Neu laden, um das Update zu installieren"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
@@ -402,7 +654,7 @@
|
||||
"message": "Wird gelöscht..."
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button": {
|
||||
"message": "Kopieren"
|
||||
"message": "Duplizieren"
|
||||
},
|
||||
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
|
||||
"message": "Kann während der Installation nicht dupliziert werden."
|
||||
@@ -488,6 +740,24 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java und Arbeitsspeicher"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-environment-variables": {
|
||||
"message": "Benutzerdefinierte Umgebungsvariablen"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-arguments": {
|
||||
"message": "Benutzerdefinierte Java-Argumente"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-installation": {
|
||||
"message": "Benutzerdefinierte Java-Installation"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-memory-allocation": {
|
||||
"message": "Benutzerdefinierte Speicherzuweisung"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Umgebungsvariablen eingeben..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Java-Argumente eingeben..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Umgebungsvariablen"
|
||||
},
|
||||
@@ -503,6 +773,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Zugewiesener Arbeitsspeicher"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/pfad/zu/java"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Fenster"
|
||||
},
|
||||
@@ -575,6 +848,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Welt wird aktuell benutzt"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Konto hinzufügen"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft-Konto"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Nicht angemeldet"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Konto entfernen"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Konto auswählen"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Bei Minecraft anmelden"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Von der Instanz vorgegeben"
|
||||
},
|
||||
@@ -598,5 +889,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader vom Server vorgegeben"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth hochgeladen wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Diese Warnung nicht mehr anzeigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Installation bestätigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Trotzdem installieren"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Schadsoftware wird häufig über Modpack-Dateien verbreitet, indem diese auf Plattformen wie Discord geteilt werden."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Wir konnten diese Datei auf Modrinth nicht finden. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Warnung vor unbekannter Datei"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,129 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Downloading Java {version}"
|
||||
},
|
||||
"app.action-bar.downloading-update": {
|
||||
"message": "Downloading update"
|
||||
},
|
||||
"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.reload-to-update": {
|
||||
"message": "Reload to update"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Show more running instances"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Stop instance"
|
||||
},
|
||||
"app.action-bar.update": {
|
||||
"message": "Update"
|
||||
},
|
||||
"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.show-play-time.description": {
|
||||
"message": "Displays how much time you've spent playing an instance."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Show play time"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Cannot reach authentication servers"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Add server to instance"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Add servers to your instance"
|
||||
"message": "Adding server to instance"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Add to an instance"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Add to instance"
|
||||
@@ -23,6 +137,9 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "Already added"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Back to instance"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Discover content"
|
||||
},
|
||||
@@ -30,13 +147,25 @@
|
||||
"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"
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installing"
|
||||
},
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "No available versions match {compatibilityLabel}. Select a version to install anyway. Dependencies will not be installed automatically."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installing modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Enter modpack description..."
|
||||
@@ -47,6 +176,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Export modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Include \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Name"
|
||||
},
|
||||
@@ -54,7 +186,7 @@
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Select files and folders to include in pack"
|
||||
"message": "Configure which files are included in this export"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Version number"
|
||||
@@ -185,6 +317,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "An update is required to play {name}. Please update to the latest version to launch the game."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "This project is already installed"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Back to discover"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Developer mode enabled."
|
||||
},
|
||||
@@ -209,8 +350,146 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Resource management"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App."
|
||||
"app.skins.add-button": {
|
||||
"message": "Add skin"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Drag and drop"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Apply"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Delete skin"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "This will permanently delete the selected skin. This action cannot be undone."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "Are you sure you want to delete this skin?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Failed to read the dropped file."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Error processing file"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Edit skin"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Add skin"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Adding a skin"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Arm style"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Slim"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Wide"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Cape"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Cape"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Editing skin"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "Make an edit to the skin first!"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "No cape"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "None"
|
||||
},
|
||||
"app.skins.modal.replace-texture-button": {
|
||||
"message": "Replace texture"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Save skin"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Saving..."
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Texture"
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "Upload a skin first!"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Edit skin"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Previewing"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "Slow down!"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chase the Skies"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Default skins"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "You received these skins for donating to a Modrinth Pride fundraiser during Pride Month."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Mounts of Mayhem"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Saved skins"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "The Copper Age"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "The Garden Awakens"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Tiny Takeover"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Sign In"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Please sign into your Minecraft account to use the skin management features of the Modrinth app."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Excited Modrinth Bot"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Please sign in"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Skin selector"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} has finished downloading. Reload to update now, or automatically when you close Modrinth App."
|
||||
@@ -231,7 +510,7 @@
|
||||
"message": "Download complete"
|
||||
},
|
||||
"app.update-popup.reload": {
|
||||
"message": "Reload"
|
||||
"message": "Reload to update"
|
||||
},
|
||||
"app.update-popup.title": {
|
||||
"message": "Update available"
|
||||
@@ -242,15 +521,6 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Version {version} was successfully installed!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Download update"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Downloading update ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Reload to install update"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
@@ -440,6 +710,30 @@
|
||||
"instance.settings.tabs.general.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel": {
|
||||
"message": "Update channel"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.alpha": {
|
||||
"message": "Alpha"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.alpha.description": {
|
||||
"message": "Release, beta, and alpha versions will be shown as available updates."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.beta": {
|
||||
"message": "Beta"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.beta.description": {
|
||||
"message": "Release and beta versions will be shown as available updates."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.release": {
|
||||
"message": "Release"
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.release.description": {
|
||||
"message": "Only release versions will be shown as available updates."
|
||||
},
|
||||
"instance.settings.tabs.general.update-channel.select": {
|
||||
"message": "Select update channel"
|
||||
},
|
||||
"instance.settings.tabs.hooks": {
|
||||
"message": "Launch hooks"
|
||||
},
|
||||
@@ -488,6 +782,24 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java and memory"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-environment-variables": {
|
||||
"message": "Custom environment variables"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-arguments": {
|
||||
"message": "Custom Java arguments"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-installation": {
|
||||
"message": "Custom Java installation"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-memory-allocation": {
|
||||
"message": "Custom memory allocation"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Enter environmental variables..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Enter Java arguments..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Environment variables"
|
||||
},
|
||||
@@ -503,6 +815,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Memory allocated"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/path/to/java"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Window"
|
||||
},
|
||||
@@ -575,6 +890,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "World is in use"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Add account"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft account"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Not signed in"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Remove account"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Select account"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Sign in to Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Provided by the instance"
|
||||
},
|
||||
@@ -598,5 +931,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,123 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Descargando Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Descargas"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ocultar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Hacer instancia principal"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "No hay instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Desconectado"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instancia principal"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Mostrar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Detener instancia"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Ver descargas en curso"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Ver instancia"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Ver registros"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Activa el renderizado avanzado, como los efectos de desenfoque, que pueden afectar el rendimiento sin aceleración por hardware."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderizado avanzado"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Selecciona el tema de color que prefieras para la Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Color de la interfaz"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Cambia la página en la que se abre el launcher."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Inicio"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Librería"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Página de inicio predeterminada"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Desactiva la etiqueta de nombre arriba de tu personaje en la página de skins."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Ocultar etiqueta de nombre"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Incluye los mundos recientes en la sección \"Volver a jugar\" de la página de inicio."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Volver a jugar mundos"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimiza el launcher al iniciar un proceso de Minecraft."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizar launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usar el borde de ventana del sistema (requiere reiniciar la aplicación)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Decoraciones nativas"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Selecciona una opción"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Mostrar tiempo de juego"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Permite mostrar u ocultar la barra lateral."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Mostrar u ocultar la barra lateral"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Si intentas instalar un paquete de Modrinth (.mrpack) que no está alojado en Modrinth, te explicaremos los riesgos antes de instalarlo."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Advertir antes de instalar modpacks desconocidos"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "No se puede acceder a los servidores de autenticación"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Añadir servidor a la instancia"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Añadir servidor a tu instancia"
|
||||
"message": "Añadiendo servidor a instancia"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Añadir a una instancia"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Añadir a la instancia"
|
||||
"message": "Añadir a instancia"
|
||||
},
|
||||
"app.browse.add-to-instance-name": {
|
||||
"message": "Añadir a {instanceName}"
|
||||
@@ -21,7 +126,10 @@
|
||||
"message": "Añadido"
|
||||
},
|
||||
"app.browse.already-added": {
|
||||
"message": "Ya está añadido"
|
||||
"message": "Ya se ha añadido"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Volver a instancia"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Descubrir contenido"
|
||||
@@ -30,16 +138,28 @@
|
||||
"message": "Descubrir servidores"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ocultar servidores añadidos"
|
||||
"message": "Ocultar servidores ya añadidos"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Ocultar contenido instalado"
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instalar contenido a una instancia"
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Añadir contenido puede romper la compatibilidad cuando te unes al servidor. Cualquier contenido añadido tambien se perderá cuando actualizes el contenido en la instancia del servidor."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Instalando"
|
||||
},
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalando modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Colocá la descripción del modpack..."
|
||||
"message": "Introduce la descripción del modpack..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Exportar"
|
||||
@@ -47,6 +167,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "¿Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
@@ -54,10 +177,10 @@
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Selelecciona archivos y carpetas para incluir en el pack"
|
||||
"message": "Configura que archivos incluir en esta exportación"
|
||||
},
|
||||
"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 +198,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 +213,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 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"
|
||||
@@ -110,14 +233,11 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Explorar servidores"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' será **permanentemente eliminado**, y habrá forma de recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "¿Estás seguro de qué quieres eliminar permanentemente este mundo?"
|
||||
"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 +252,13 @@
|
||||
"message": "Añadir un servidor o explora para comenzar"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "No hay servidores o mundos añadidos"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) será eliminado de tu lista, incluyendo dentro del juego, y no habrá forma de recuperarlo."
|
||||
"message": "No hay servidores ni mundos añadidos"
|
||||
},
|
||||
"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"
|
||||
@@ -153,7 +267,7 @@
|
||||
"message": "Contenido requerido"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Instala para jugar"
|
||||
"message": "Instalar para jugar"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Instalar"
|
||||
@@ -165,7 +279,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"
|
||||
@@ -177,7 +291,7 @@
|
||||
"message": "Ver contenidos"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Actualiza para jugar"
|
||||
"message": "Actualizar para jugar"
|
||||
},
|
||||
"app.modal.update-to-play.update-required": {
|
||||
"message": "Actualización requerida"
|
||||
@@ -185,6 +299,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Se requiere una actualización para jugar {name}. Por favor, actualiza a la versión más reciente para iniciar el juego."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Este proyecto ya está instalado"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Volver a descubrir contenido"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Instalar contenido a la instancia"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Modo desarrollador activado."
|
||||
},
|
||||
@@ -209,8 +332,146 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Gestión de recursos"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "¡Modrinth App v{version} está lista para instalarse! Actualiza ahora o automáticamente al cerrar la Modrinth App."
|
||||
"app.skins.add-button": {
|
||||
"message": "Añadir skin"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Arrastrar y soltar"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Aplicar"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Eliminar skin"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Esto eliminará permanentemente el skin seleccionado. Esta acción no se puede deshacer."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "¿Estás seguro de que quieres eliminar este skin?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Error al leer el archivo arrastrado."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Error al procesar el archivo"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Editar skin"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Añadir skin"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Añadiendo un skin"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Estilo de brazo"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Delgado"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Ancho"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Capa"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Capa"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Editando skin"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "¡Primero edita el skin!"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Sin capa"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Ninguna"
|
||||
},
|
||||
"app.skins.modal.replace-texture-button": {
|
||||
"message": "Remplazar textura"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Guardar skin"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Guardando..."
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Textura"
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "¡Primero carga un skin!"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Editar skin"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Previsualizando"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "Estás cambiando tu skin muy frecuentemente. Los servidores de Mojang han bloqueado temporalmente las solicitdues futuras. Por favor, espera un momento antes de volver a intentarlo."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "¡Más despacio!"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Constructores y Biomas"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Persigue los Cielos"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Skins predeterminadas"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Orgullo Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "Has recibido estas skins por donar a la fundación Orgullo Modrinth durante el Mes del Orgullo."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Monturas del Caos"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Skins guardados"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "La Era del Cobre"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "El Jardín Despierta"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Bebés al Poder"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Iniciar sesión"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Por favor, inicia sesión en tu cuenta de Minecraft para poder utilizar las funciones de gestión de skins de la Modrinth App."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Modrinth Bot emocionado"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Por favor, inicia sesión"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Selector de skin"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "La descarga de la Modrinth App v{version} ha finalizado. Actualiza ahora o automáticamente al cerrar la Modrinth App."
|
||||
@@ -242,15 +503,6 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "¡La versión {version} se ha instalado correctamente!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Descargar actualización"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Descargando actualización ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Recarga para instalar la actualización"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "ejemplo.modrinth.gg"
|
||||
},
|
||||
@@ -261,7 +513,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 +537,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?"
|
||||
@@ -488,6 +740,24 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java y memoria"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-environment-variables": {
|
||||
"message": "Variables de entorno personalizadas"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-arguments": {
|
||||
"message": "Argumentos personalizados de Java"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-installation": {
|
||||
"message": "Instalación personalizada de Java"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-memory-allocation": {
|
||||
"message": "Asignación de memoria personalizada"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Introduce las variables de entorno..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Introduce los argumentos de Java..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Variables de entorno"
|
||||
},
|
||||
@@ -503,6 +773,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Memoria asignada"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/ruta/de/java"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Ventana"
|
||||
},
|
||||
@@ -575,6 +848,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "El mundo ya está en uso"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Añadir cuenta"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Cuenta de Minecraft"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "No has inciado sesión"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Eliminar cuenta"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Seleccionar cuenta"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Inicia sesión en Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Proporcionado por la instancia"
|
||||
},
|
||||
@@ -598,5 +889,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "El loader es proporcionado por el servidor"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "No volver a mostrar esta advertencia"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirmar instalación"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Instalar de todos modos"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "El malware a menudo es distribuido mediante modpacks compartidos en aplicaciones como Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "No pudimos encontrar este archivo en Modrinth. Recomendamos solo instalar archivos de sitios de confianza."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Advertencia de archivo desconocido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,120 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Descargando Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Descargas"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ocultar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Establecer como instancia principal"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Ninguna instancia en ejecución"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Sin conexión"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instancia principal"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Mostrar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Detener instancia"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Mostrar descargas activas"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Ver instancia"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Ver registros"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Habilita el renderizado avanzado, como los efectos de desenfoque, que pueden causar problemas de rendimiento sin renderizado acelerado por hardware."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderizado avanzado"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Selecciona tu tema de color preferido para la Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema de color"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Cambia la página que se abre al iniciar el lanzador."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Inicio"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Biblioteca"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Página de inicio predeterminada"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Desactiva la etiqueta arriba de tu personaje en la página de skins."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Ocultar etiqueta de nombre"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Incluye los mundos recientes en la sección \"Volver a jugar\" de la página de inicio."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Volver a jugar en tus mundos"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimiza el lanzador cuando se inicia un proceso de Minecraft."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizar lanzador"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa el marco de ventana del sistema (requiere reiniciar la aplicación)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Decoraciones nativas"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Selecciona una opción"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Muestra cuánto tiempo has estado jugando en una instancia."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Mostrar tiempo de juego"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Activa la posibilidad de alternar la barra lateral."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Alternar barra lateral"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Si intentas instalar un archivo de paquete de Modrinth (.mrpack) que no esté alojado en Modrinth, debes comprender los riesgos que esto conlleva antes de instalarlo."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Adviérteme antes de instalarme modpacks desconocidos"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Los servidores de autenticación de Minecraft pueden no estar funcionando en este momento. Verifica tu conexión a internet e inténtalo de nuevo más tarde."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "No se puede conectar con los servidores de autenticación"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Añadir servidor a la instancia"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Añadir servidor a tu instancia"
|
||||
"message": "Añadiendo servidor a la instancia"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Añadir a una instancia"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Añadir a la instancia"
|
||||
@@ -23,6 +128,9 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "Ya está añadido"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Volver a la instancia"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Descubrir contenido"
|
||||
},
|
||||
@@ -30,13 +138,25 @@
|
||||
"message": "Descubrir servidores"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ocultar servidores añadidos"
|
||||
"message": "Ocultar servidores ya añadidos"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Ocultar contenido instalado"
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instalar contenido a una instancia"
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Añadir contenido puede comprometer la compatibilidad a la hora de unirse a un servidor. Cualquier contenido añadido se perderá cuando actualices la instancia del servidor."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Instalando"
|
||||
},
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "No hay versiones disponibles que coincidan con {compatibilityLabel}. Selecciona una versión para instalarla de todas formas. Las dependencias no se instalarán automáticamente."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalando modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Escribe la descripción del modpack..."
|
||||
@@ -47,6 +167,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exportar modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "¿Incluir \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
@@ -54,7 +177,7 @@
|
||||
"message": "Nombre del modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Seleccione archivos y carpetas para incluir en el paquete"
|
||||
"message": "Configura que archivos se incluirán en esta instancia"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Número de versión"
|
||||
@@ -165,7 +288,7 @@
|
||||
"message": "Modpack requerido"
|
||||
},
|
||||
"app.modal.install-to-play.server-requires-mods": {
|
||||
"message": "Este servidor requiere ciertos mods. Pulsa Instalar para instalar los archivos requeridos de Modrinth y luego el Launcher te enviara directo al servidor."
|
||||
"message": "Este servidor requiere ciertos mods. Pulsa Instalar para instalar los archivos requeridos de Modrinth y luego el launcher te enviara directo al servidor."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Instancia compartida"
|
||||
@@ -185,6 +308,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Una actualización es requerida para jugar {name}. Por favor actualízala a la versión más reciente para ejecutar el juego."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "El proyecto ya está instalado"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Volver a descubrir"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Instalar contenido"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Modo desarrollador activado."
|
||||
},
|
||||
@@ -209,8 +341,146 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Gestión de recursos"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "¡La versión v{version} de Modrinth está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación."
|
||||
"app.skins.add-button": {
|
||||
"message": "Añadir skin"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Arrastrar y soltar"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Aplicar"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Borrar skin"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Esto borrará permanentemente la skin seleccionada. Esta acción no se puede deshacer."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "¿Estás seguro/a de que quieres eliminar está skin?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Error al leer el archivo arrastrado."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Error al procesar el archivo"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Editar skin"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Añadir skin"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Añadiendo una skin"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Estilo de brazo"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Delgado"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Ancho"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Capa"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Capa"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Editando skin"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "¡Modifica la skin primero!"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Sin capa"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Nada"
|
||||
},
|
||||
"app.skins.modal.replace-texture-button": {
|
||||
"message": "Reemplazar textura"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Guardar skin"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Guardando..."
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Textura"
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "¡Primero sube una skin!"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Editar skin"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Previsualizando"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "Estuviste cambiando tu skin demasiado rápido. Los servidores de Mojang han bloqueado temporalmente tus futuras solicitaciones. Por favor, espere un momento antes de intentar de nuevo."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "¡Cálmate!"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders y Biomas"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Persigue los cielos"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Skins por defecto"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Orgullo Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "Has recibido estas skins por donar a la fundación Orgullo Modrinth durante el Mes del Orgullo."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Monturas del Caos"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Skins guardadas"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "La Edad de Cobre"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "El Jardín Despierta"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Pequeña Invasión"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Iniciar Sesión"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Por favor, inicia sesión con tu cuenta de Minecraft para usar las funciones del administrador de Skins de la app de Modrinth."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Modrinth Bot emocionado"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Por favor, inicia sesión"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Selector de Skin"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} se ha descargado. Actualiza la página ahora o espera a que se actualice automáticamente al cerrar Modrinth App."
|
||||
@@ -242,15 +512,6 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "¡La versión {version} se ha instalado correctamente!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Descarga actualización"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Descargando actualización ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Recarga para instalar la actualización"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "ejemplo.modrinth.gg"
|
||||
},
|
||||
@@ -488,6 +749,24 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java y memoria"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-environment-variables": {
|
||||
"message": "Variables de entorno personalizadas"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-arguments": {
|
||||
"message": "Argumentos personalizados de Java"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-installation": {
|
||||
"message": "Instalación personalizada de Java"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-memory-allocation": {
|
||||
"message": "Asignación de memoria personalizada"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Introduce las variables de entorno..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Introduce los argumentos de Java..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Variables de entorno"
|
||||
},
|
||||
@@ -503,6 +782,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Memoria asignada"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/ruta/a/java"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Ventana"
|
||||
},
|
||||
@@ -575,6 +857,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "El mundo está en uso"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Añadir cuenta"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Cuenta de Minecraft"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Registrarse"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Eliminar cuenta"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Seleccionar cuenta"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Registrarse en Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Proporcionado por la instancia"
|
||||
},
|
||||
@@ -598,5 +898,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "El cargador lo proporciona el servidor"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "No mostrar esta advertencia otra vez"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirmar instalación"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Instalar de todos modos"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "El malware es normalmente distribuido por archivos de modpack que son normalmente compartidas en aplicaciones como Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "No pudimos encontrar este archivo en Modrinth. Recomendamos solo instalar archivos de sitios de confianza."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Advertencia de archivo desconocido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,174 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Ladataan Java-versiota {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Lataukset"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Piilota enemmän käynnissä olevia instansseja"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Aseta ensisijaiseksi instanssiksi"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Ei instansseja käynnissä"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline-tilassa"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Ensisijainen instanssi"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Näytä enemmän käynnissä olevia instansseja"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Pysäytä instanssi"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Näytä aktiiviset lataukset"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Katso intanssia"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Katso lokeja"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Mahdollistaa edistyneen renderöinnin, kuten sumennus efektit, jotka voivat aiheuttaa suorituskykyongelmia ilman laitteistokiihdytettyä renderöintiä."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Edistynyt renderointi"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Valitse haluamasi väriteema Modrinth Appille."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Väriteema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Muuta sivua mille laukaisin avautuu."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Koti"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Kirjasto"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Oletus laskeutumissivu"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Kytkee pois päältä nimikyltin pelaajasi päällä skinit sivulla."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Piilota nimikyltti"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Sisällyttää viimeaikaiset maailmat \"Hyppää takaisin sisään\" osiossa Koti sivulla."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Hyppää takaisin sisään maailmoihin"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Pienennä laukaisin kun Minecraft prosessi käynnistyy."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Piilota käynnistysohjelma"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Käytä järjestelmän ikkuna kehystä (Vaatii sovelluksen uudelleen käynnistämisen)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Natiivit koristukset"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Valitse vaihtoehto"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Laittaa päälle mahdollisuuden kytkeä sivupalkkia."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Kytke sivupalkki"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Jos yrität asentaa Modrinth-pakettitiedoston (.mrpack), jota ei ole isännöity Modrinthissä, varmistamme, että ymmärrät riskit ennen asennusta."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Varoita minua ennen tuntemattomien modipakettien asentamista"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraftin todennuspalvelimet eivät ehkä ole tällä hetkellä tavoitettavissa. Tarkista internetyhteytesi ja yritä myöhemmin uudelleen."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Todennuspalvelimiin ei saada yhteyttä"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Lisää palvelin instanssiin"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Lisää instanssiin"
|
||||
},
|
||||
"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.back-to-instance": {
|
||||
"message": "Takaisin instanssiin"
|
||||
},
|
||||
"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.project-type.modpacks": {
|
||||
"message": "Modipaketit"
|
||||
},
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Sisällön lisääminen voi rikkoa yhteensopivuuden palvelimelle liittyessä. Kaikki lisätty sisältö katoaa myös kun päivität palvelin instanssin sisällön."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Asennetaan"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Asennetaan modipakettia..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Lisää modipaketin kuvaus..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Vie"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Vie modipaketti"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Sisällytä \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Modpack nimi"
|
||||
"message": "Modipaketin nimi"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Valitse tiedostot ja kansiot pakettiin"
|
||||
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Versio numero"
|
||||
@@ -17,12 +176,93 @@
|
||||
"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.share-title": {
|
||||
"message": "Jaetaan modipaketti sisältöä"
|
||||
},
|
||||
"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 +273,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"
|
||||
@@ -59,6 +299,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Pävitys vaaditaan pelataksesi {name}. Päivitä viimeisimpään versioon pelataksesi."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Tämä projekti on jo asennettu"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Takaisin sisällön löytöön"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Asenna sisältöä instanssiin"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Kehittäjätila käytössä."
|
||||
},
|
||||
@@ -83,9 +332,6 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Resurssien hallinta"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Modrinth App versio {version} on valmis asennettavaksi. Voit käynnistää sovelluksen uudelleen päivittääksesi heti tai antaa päivityksen asentua automaattisesti, kun suljet Modrinth Appin."
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App versio {version} on ladattu. Voit käynnistää sovelluksen uudelleen päivittääksesi heti tai antaa päivityksen asentua automaattisesti, kun suljet Modrinth Appin."
|
||||
},
|
||||
@@ -116,14 +362,23 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Versio {version} asennettiin onnistuneesti!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Lataa päivitys"
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Ladataan päivitystä ({percent}%)"
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Valitse vaihtoehto"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Lataa uudelleen asentaaksesi päivityksen"
|
||||
"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 +479,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"
|
||||
},
|
||||
@@ -338,6 +599,24 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java ja muisti"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-environment-variables": {
|
||||
"message": "Mukautetut ympäristö muuttujat"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-arguments": {
|
||||
"message": "Mukautetut Java argumentit"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-installation": {
|
||||
"message": "Mukautettu Java asennus"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-memory-allocation": {
|
||||
"message": "Mukautettu muistin allokointi"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Syötä ympäristö muuttujat..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Syötä Java argumentit..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Ympäristö muuttujat"
|
||||
},
|
||||
@@ -353,6 +632,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Muisti allokoitu"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/javaan/johtava/polku/"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Ikkuna"
|
||||
},
|
||||
@@ -425,6 +707,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Maailma on käytössä"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Lisää tili"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft tili"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Ei kirjautuneena sisään"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Poista tili"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Valitse tili"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Kirjaudu sisään Minecraftiin"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Annettu instanssin toimesta"
|
||||
},
|
||||
@@ -441,12 +741,33 @@
|
||||
"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"
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Modialusta on palvelimen tarjoama"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Tiedosto tarkistetaan vain jos se on ladattu Modrinthiin, riippumatta tiedoston muodosta (mukaanlukien .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Älä näytä tätä varoitusta uudestaan"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Vahvista asennus"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Asenna jokatapauksessa"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Haittaohjelmia levitetään usein modipaketti tiedostojen kautta jakamalla niitä alustoilla kuten Discordissa."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Emme löytäneet tätä tiedostoa Modrinthista. Suosittelemme vahvasti että asennat tiedostoja vain lähteistä joihin luotat."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Tuntematon tiedosto varoitus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,268 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Dina-download ang Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Mga downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Itago ang mas marami pang tumatakbong instansiya"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Gumawa ng pangunahing instansiya"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Walang instansiya ang tumatakbo"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Pangunahing instansiya"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Ipakita ang mas marami pang tumatakbong instansiya"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Itigil ang instansiya"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Tignan ang mga active downloads"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Tignan ang instansiya"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Tignan ang logs"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Pagpapagana ng masalimuot na pagrerender katulad ng paglalabo na maaaring magdudulot ng mga isyu sa performance kapag walang hardware-acceleration."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Masalimuot na pagrerender"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Piliin ang iyong mas gustong kulay para sa Modrinth app."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema ng kulay"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Pagpapalit ng pahina na bubuksan ng launcher."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Tahanan"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Librarya"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Panimulang pahina ng paglapag"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Pagtatago ng nametag na nasa itaas ng iyong manlalaro sa loob ng pahina ng mga skin."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Itago ang nametag"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Pagsasali ng mga kamakailang mundo sa seksiyong \"Jump back in\" sa pahina ng Tahanan."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Bumalik sa mga mundo"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Liitan ang launcher kung nag-start ang isang proseso ng Minecraft."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Liitin ang launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Gamitin ang system window frame (kailangan i-restart ang app)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Native decorations"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Pumili ng opsiyon"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Pagpapagana na mata-toggle ang sidebar."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "I-toggle ang sidebar"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Kung susubukan mong mag-install ng Modrinth Pack file (.mrpack) na hindi naka-host sa Modrinth, titiyakin namin na nakaaalam ka sa mga panganib bago ito ma-install."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Pakipaalala bago ko ma-install ang mga di-kilalang modpack"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Maaaring hindi maaabot ang mga authentication server ng Minecraft sa ngayon. Tingnan mo ang iyong internet connection at muling subukan mamaya."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Hindi maabot ang mga authentication server"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Idinaragdag ang server sa instansiya"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Idagdag sa 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.back-to-instance": {
|
||||
"message": "Bumalik sa instansiya"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Tumuklas ng kontento"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Tumuklas ng mga server"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Itago ang mga nailagay na mga servers"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Maaaring makasisira sa pagkakatugma sa pagsasali ng server ang pagdaragdag ng kontento. Ang anumang kontento ay mawawala sa pag-update ng kontento ng instansiyang pang-server."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Ini-install"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Ini-install ang modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Ilagay ang paglalarawan ng modpack..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Iluwas"
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Iluwas ang modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Salihin ang \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Pangalan ng Modpack"
|
||||
},
|
||||
"app.export-modal.modpack-name-placeholder": {
|
||||
"message": "Pangalan ng modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numero ng bersiyon"
|
||||
},
|
||||
"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.browse-servers": {
|
||||
"message": "Mag-browse ng servers"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "'{name}' ay **permanenteng mabubura**, at walang paraan upang maibalik muli."
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "Sigurado ka bang gusto mong permanenteng burahin ang world na ito?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Modded"
|
||||
},
|
||||
"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": "Maglagay ng server o mag-browse upang mag-simula"
|
||||
},
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Walang servers o worlds ang nalalagay"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "'{name}' ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "'{name}' ({address}) ay matatangal sa iyong listahan, pati sa loob ng laro, at walang paraan upang maibalik ito."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Sigurado ka bang gusto mong tanggalin ang {name}?"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -65,8 +299,14 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Kailangang mag-update upang malaro ang {name}. Mangyaring mag-update sa pinakabagong bersiyon upang ma-launch ang laro."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Naka-install na ang proyektong ito"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Bumalik sa pagtutuklas"
|
||||
},
|
||||
"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}"
|
||||
@@ -89,8 +329,35 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"message": "Pamamahala ng paglalaan"
|
||||
},
|
||||
"app.update-popup.body": {
|
||||
"message": "Ang Modrinth App v{version} ay handa nang ma-install. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
|
||||
"app.skins.apply-button": {
|
||||
"message": "Ilapat"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Payat"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Malapad"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Kapa"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Kapa"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Walang kapa"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Wala"
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Texture"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Tapos nang ma-download ang Modrinth App v{version}. Mag-reload upang ma-update ngayon, o awtomatiko sa pagsara ng Modrinth App."
|
||||
@@ -122,14 +389,23 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "Tagumpay na na-install ang bersiyong {version}!"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "I-download ang update"
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "example.modrinth.gg"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Nagdadownload ng update ({percent}%)"
|
||||
"app.world.server-modal.select-an-option": {
|
||||
"message": "Pumili ng opsiyon"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Handang ma-install ang update"
|
||||
"app.world.world-item.incompatible-version": {
|
||||
"message": "Di-magkatugmang bersiyong {version}"
|
||||
},
|
||||
"app.world.world-item.not-played-yet": {
|
||||
"message": "Hindi pa nalalaro"
|
||||
},
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} online"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Magdagdag ng kaibigan"
|
||||
@@ -201,10 +477,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 +492,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"
|
||||
@@ -230,6 +506,12 @@
|
||||
"instance.edit-world.title": {
|
||||
"message": "Baguhin ang mundo"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "Idinadagdag ang mga file ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "I-save bilang..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Adres"
|
||||
},
|
||||
@@ -399,7 +681,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"
|
||||
@@ -431,6 +713,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Ginagamit ang mundo"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Maglagay ng account"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft account"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Hindi pa naka sign in"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Itanggal ang account"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Pumili ng account"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Mag sign in kay Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Sagot na ng instansiya"
|
||||
},
|
||||
@@ -454,5 +754,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Ang loader ay handog na ng server"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Ang file ay nasusuri lamang kapag ito ay na-upload sa Modrinth, walang pili sa file format nito (kabilang na ang .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Huwag ipakita ang babalang ito ulit"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Kumpirmahin ang installation"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "I-install pa rin"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Ang malware ay madalas naidadala sa mga modpack files sa pamamagitan ng pag-bigay ng mga ito sa mga platforms kagaya ng Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Hindi namin mahanap ang file na ito sa Modrinth. Mahalagang mag-install ka lamang ng files galing sa mga mapagkakatiwalang sources."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Hindi kilalang file"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,120 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Téléchargement de Java {version} en cours"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Téléchargements"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Masquer plus d'instances en exécution"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Définir en tante qu'instance première"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Aucune instance en exécution"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Hors ligne"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instance première"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Montrer plus d'instances en exécution"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Arrêter l'instance"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Voir les téléchargements actifs"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Voir l'instance"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Voir les journaux"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Permet un rendu avancé tel que des effets de flou qui peuvent causer des problèmes de performance sans rendu accéléré par le matériel."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Rendu avancé"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Sélectionnez votre couleur préférée pour le thème de Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Couleur du thème"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Modifier la page sur laquelle le lanceur s'ouvre."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Accueil"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Bibliothèque"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Page d'ouverture"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Désactive le nom au-dessus du joueur sur la page skins."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Cacher le pseudo"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Inclut les mondes récents dans la section « Partie rapide » sur la page d'accueil."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Mondes sur partie rapide"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimiser le lanceur quand Minecraft démarre."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimiser le lanceur"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Utiliser le cadre fenêtre du système (redémarrage de l'application requis)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Décorations natives"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Sélectionnez une option"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Affiche combien de temps vous avez passé sur une instance."
|
||||
},
|
||||
"app.appearance-settings.show-play-time.title": {
|
||||
"message": "Afficher le temps de jeu"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Permet d'ouvrir de fermer la barre latérale."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Tiroir latéral"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Si vous essayez d'installer un fichier Modrinth Pack (.mrpack) qui n'est pas hébergé sur Modrinth, nous nous assurerons que vous comprenez les risques avant de l'installer."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "M'avertir avant d'installer des modpacks inconnus"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Les serveurs d'authentification de Minecraft sont peut-être actuellement hors ligne. Vérifiez votre connexion Internet et essayez à nouveau dans quelques instants."
|
||||
},
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Impossible de contacter les serveurs d'authentification"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Ajouter le serveur a l'instance"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Ajouter des serveurs a votre instance"
|
||||
"message": "Ajouter le serveur à l'instance"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Ajouter à une instance"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Ajouter à l'instance"
|
||||
@@ -23,6 +128,9 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "Déjà ajouté"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Retour à l'instance"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Découvrir du contenu"
|
||||
},
|
||||
@@ -30,13 +138,25 @@
|
||||
"message": "Découvrir des serveurs"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Masquer les serveurs ajoutés"
|
||||
"message": "Masquer les serveurs déjà ajoutés"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Masquer le contenu installé"
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Installer du contenu à l'instance"
|
||||
"app.browse.server-instance-content-warning": {
|
||||
"message": "Ajouter du contenu peut briser la comptabilité en rejoignant le serveur. Tout contenu en plus sera également perdu lorsque vous mettrez à jour le contenu de l'instance du serveur."
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installation en cours"
|
||||
},
|
||||
"app.content-install.no-compatible-versions": {
|
||||
"message": "Aucune version disponible ne correspond à {compatibilityLabel}. Sélectionnez une version pour l'installer malgré tout. Les dépendances ne seront pas installées automatiquement."
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installation du modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Saisir la description du modpack..."
|
||||
@@ -47,6 +167,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Exporter le modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Inclure « {file} » ?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Nom du modpack"
|
||||
},
|
||||
@@ -54,7 +177,7 @@
|
||||
"message": "Nom du modpack"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Sélectionnez des fichiers et des dossiers à inclure dans le pack"
|
||||
"message": "Configurez quels fichiers sont inclus dans cette exportation"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Numéro de version"
|
||||
@@ -63,7 +186,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"
|
||||
@@ -75,7 +198,7 @@
|
||||
"message": "Supprimer l'instance"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr.e de vouloir le dupliquer ?"
|
||||
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr·e de vouloir le dupliquer ?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "Créer"
|
||||
@@ -99,7 +222,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"
|
||||
@@ -110,11 +233,8 @@
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Parcourir des serveurs"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"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é"
|
||||
@@ -134,14 +254,8 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Aucun serveur ou monde ajoutés"
|
||||
},
|
||||
"app.instance.worlds.remove-server-description": {
|
||||
"message": "« {name} » sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
|
||||
},
|
||||
"app.instance.worlds.remove-server-description-with-address": {
|
||||
"message": "« {name} » ({address}) sera retiré de votre liste, y compris en jeu, et il sera impossible de le récupérer."
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "Êtes-vous sûr.e de vouloir retirer {name} ?"
|
||||
"message": "Êtes-vous sûr·e de vouloir retirer {name} ?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Rechercher {count} mondes..."
|
||||
@@ -185,6 +299,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Une mise à jour est requise pour jouer à {name}. Veuillez mettre à jour à la dernière version pour lancer le jeu."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Ce projet a déjà été installé"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Retour vers la page découvrir"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Installer du contenu à l'instance"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Mode développeur activé."
|
||||
},
|
||||
@@ -209,8 +332,146 @@
|
||||
"app.settings.tabs.resource-management": {
|
||||
"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."
|
||||
"app.skins.add-button": {
|
||||
"message": "Ajouter une skin"
|
||||
},
|
||||
"app.skins.add-button.drag-and-drop": {
|
||||
"message": "Glisser et déposer"
|
||||
},
|
||||
"app.skins.apply-button": {
|
||||
"message": "Appliquer"
|
||||
},
|
||||
"app.skins.delete-button": {
|
||||
"message": "Supprimer le skin"
|
||||
},
|
||||
"app.skins.delete-modal.description": {
|
||||
"message": "Cela va supprimer de manière permanente ce skin. Cette action est irréversible."
|
||||
},
|
||||
"app.skins.delete-modal.title": {
|
||||
"message": "Êtes-vous sûr de vouloir supprimer cette skin ?"
|
||||
},
|
||||
"app.skins.dropped-file-error.text": {
|
||||
"message": "Impossible de lire le fichier déposé."
|
||||
},
|
||||
"app.skins.dropped-file-error.title": {
|
||||
"message": "Erreur lors du traitement du fichier"
|
||||
},
|
||||
"app.skins.edit-button": {
|
||||
"message": "Modifier le skin"
|
||||
},
|
||||
"app.skins.modal.add-skin-button": {
|
||||
"message": "Ajouter une skin"
|
||||
},
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Ajouter une skin"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Bras"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Mince"
|
||||
},
|
||||
"app.skins.modal.arm-style-wide": {
|
||||
"message": "Large"
|
||||
},
|
||||
"app.skins.modal.cape-fallback-name": {
|
||||
"message": "Cape"
|
||||
},
|
||||
"app.skins.modal.cape-section": {
|
||||
"message": "Cape"
|
||||
},
|
||||
"app.skins.modal.edit-title": {
|
||||
"message": "Modification du skin"
|
||||
},
|
||||
"app.skins.modal.make-edit-first-tooltip": {
|
||||
"message": "Faites d'abord une modification à la skin !"
|
||||
},
|
||||
"app.skins.modal.no-cape-tooltip": {
|
||||
"message": "Aucune cape"
|
||||
},
|
||||
"app.skins.modal.none-cape-option": {
|
||||
"message": "Aucune"
|
||||
},
|
||||
"app.skins.modal.replace-texture-button": {
|
||||
"message": "Remplacer la texture"
|
||||
},
|
||||
"app.skins.modal.save-skin-button": {
|
||||
"message": "Sauvegarder le skin"
|
||||
},
|
||||
"app.skins.modal.saving-tooltip": {
|
||||
"message": "Sauvegarde..."
|
||||
},
|
||||
"app.skins.modal.texture-section": {
|
||||
"message": "Texture"
|
||||
},
|
||||
"app.skins.modal.upload-skin-first-tooltip": {
|
||||
"message": "Importez d'abord une skin !"
|
||||
},
|
||||
"app.skins.preview.edit-button": {
|
||||
"message": "Modifier le skin"
|
||||
},
|
||||
"app.skins.previewing-badge": {
|
||||
"message": "Aperçu"
|
||||
},
|
||||
"app.skins.rate-limit.text": {
|
||||
"message": "Vous changez de skin trop vite. Les serveurs de Mojang ont temporairement bloqué certaines requêtes. Veuillez patienter un moment puis réessayez."
|
||||
},
|
||||
"app.skins.rate-limit.title": {
|
||||
"message": "Du calme !"
|
||||
},
|
||||
"app.skins.section.builders-and-biomes": {
|
||||
"message": "Builders & Biomes"
|
||||
},
|
||||
"app.skins.section.chase-the-skies": {
|
||||
"message": "Chassez les cieux"
|
||||
},
|
||||
"app.skins.section.default-skins": {
|
||||
"message": "Skins par défaut"
|
||||
},
|
||||
"app.skins.section.minecon-earth-2017": {
|
||||
"message": "MINECON Earth 2017"
|
||||
},
|
||||
"app.skins.section.modrinth": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app.skins.section.modrinth-pride": {
|
||||
"message": "Modrinth Pride"
|
||||
},
|
||||
"app.skins.section.modrinth-pride.tooltip": {
|
||||
"message": "Vous avez reçu ces skins pour avoir fait un don à une collecte de fonds Modrinth Pride pendant le mois de la fierté."
|
||||
},
|
||||
"app.skins.section.mounts-of-mayhem": {
|
||||
"message": "Montures en pagaille"
|
||||
},
|
||||
"app.skins.section.saved-skins": {
|
||||
"message": "Skins sauvegardées"
|
||||
},
|
||||
"app.skins.section.striding-hero": {
|
||||
"message": "Striding Hero"
|
||||
},
|
||||
"app.skins.section.the-copper-age": {
|
||||
"message": "The Copper Age"
|
||||
},
|
||||
"app.skins.section.the-garden-awakens": {
|
||||
"message": "L'éveil du jardin"
|
||||
},
|
||||
"app.skins.section.tiny-takeover": {
|
||||
"message": "Horde miniature"
|
||||
},
|
||||
"app.skins.sign-in.button": {
|
||||
"message": "Se connecter"
|
||||
},
|
||||
"app.skins.sign-in.description": {
|
||||
"message": "Veuillez vous connecter à votre compte Minecraft pour utiliser les fonctionnalités de gestion de skins de Modrinth App."
|
||||
},
|
||||
"app.skins.sign-in.rinthbot-alt": {
|
||||
"message": "Bot Modrinth enthousiaste"
|
||||
},
|
||||
"app.skins.sign-in.title": {
|
||||
"message": "Veuillez vous connecter"
|
||||
},
|
||||
"app.skins.title": {
|
||||
"message": "Sélecteur de skin"
|
||||
},
|
||||
"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."
|
||||
@@ -242,15 +503,6 @@
|
||||
"app.update.complete-toast.title": {
|
||||
"message": "La version {version} a été téléchargée avec succès !"
|
||||
},
|
||||
"app.update.download-update": {
|
||||
"message": "Télécharger la mise à jour"
|
||||
},
|
||||
"app.update.downloading-update": {
|
||||
"message": "Téléchargement de la mise à jour ({percent}%)"
|
||||
},
|
||||
"app.update.reload-to-update": {
|
||||
"message": "Relancez l'application pour installer la mise à jour"
|
||||
},
|
||||
"app.world.server-modal.placeholder-address": {
|
||||
"message": "exemple.modrinth.gg"
|
||||
},
|
||||
@@ -266,9 +518,6 @@
|
||||
"app.world.world-item.offline": {
|
||||
"message": "Hors ligne"
|
||||
},
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count, plural, =0 {Personne en ligne} one {# est en ligne} other {# sont en ligne}}"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Ajouter un ami"
|
||||
},
|
||||
@@ -488,6 +737,24 @@
|
||||
"instance.settings.tabs.java": {
|
||||
"message": "Java et mémoire"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-environment-variables": {
|
||||
"message": "Variables d'environnement personnalisé"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-arguments": {
|
||||
"message": "Arguments Java personnalisée"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-java-installation": {
|
||||
"message": "Installation Java personnalisée"
|
||||
},
|
||||
"instance.settings.tabs.java.custom-memory-allocation": {
|
||||
"message": "Allocation de mémoire personnalisée"
|
||||
},
|
||||
"instance.settings.tabs.java.enter-environment-variables": {
|
||||
"message": "Entrer une variable d'environnement..."
|
||||
},
|
||||
"instance.settings.tabs.java.enter-java-arguments": {
|
||||
"message": "Entrer un argument Java..."
|
||||
},
|
||||
"instance.settings.tabs.java.environment-variables": {
|
||||
"message": "Variables d'environnement"
|
||||
},
|
||||
@@ -503,6 +770,9 @@
|
||||
"instance.settings.tabs.java.java-memory": {
|
||||
"message": "Mémoire allouée"
|
||||
},
|
||||
"instance.settings.tabs.java.java-path-placeholder": {
|
||||
"message": "/chemin/vers/java"
|
||||
},
|
||||
"instance.settings.tabs.window": {
|
||||
"message": "Fenêtre"
|
||||
},
|
||||
@@ -575,6 +845,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Le monde en cours d'utilisation"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Ajouter un compte"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Compte Minecraft"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Pas connecté"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Retirer le compte"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Sélectionner un compte"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Se connecter à Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Procuré par l'instance"
|
||||
},
|
||||
@@ -598,5 +886,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Le loader est procuré par le serveur"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Un fichier n’est révisé que s’il est téléchargé sur Modrinth, quel que soit son format de fichier (y compris .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Ne plus m'avertir à ce sujet"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirmer l'installation"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Installer tout de même"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Les logiciels malveillants sont souvent distribués via des fichiers modpack en les partageant sur des plateformes comme Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Nous ne pouvions pas trouver ce fichier sur Modrinth. Nous vous recommandons fortement de n'installer que des fichiers de sources de confiance."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Avertissement fichier inconnu"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user