Compare commits

..
Author SHA1 Message Date
François-X. T. b1bd795ee8 Fix typo lint 2026-04-18 23:57:00 -04:00
1521 changed files with 32452 additions and 190146 deletions
@@ -9,6 +9,8 @@ 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:
-171
View File
@@ -1,171 +0,0 @@
# 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);
}
-125
View File
@@ -1,125 +0,0 @@
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
@@ -1,22 +0,0 @@
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 }}
+2 -2
View File
@@ -16,8 +16,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Post or update changelog comment
uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
- name: 💬 Post or update changelog comment
uses: actions/github-script@v7
with:
github-token: ${{ secrets.CROWDIN_GH_TOKEN }}
script: |
+5 -5
View File
@@ -2,7 +2,7 @@ on:
pull_request:
push:
branches:
- main
- master
env:
CARGO_TERM_COLOR: always
@@ -12,15 +12,15 @@ jobs:
typos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: crate-ci/typos@6ac2ebd1b93eade61faf7e12688ad87a073fea59 # v1.46.0
- uses: actions/checkout@v4
- uses: crate-ci/typos@v1.43.1
# see <https://github.com/influxdata/datafusion-udf-wasm/pull/275>
tombi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: taiki-e/install-action@fd2f5e3d644b484055ebf4268f474c565f148f25 # v2.81.9
- uses: actions/checkout@v4
- uses: taiki-e/install-action@v2
with:
tool: tombi
- run: tombi lint
+4 -4
View File
@@ -2,7 +2,7 @@ on:
pull_request:
push:
branches:
- main
- master
env:
CARGO_TERM_COLOR: always
@@ -12,8 +12,8 @@ jobs:
shear:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: cargo-bins/cargo-binstall@main
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
+18 -117
View File
@@ -3,133 +3,33 @@ 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:
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
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' }}
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 📥 Check out code
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
with:
rustflags: ''
cache: false
- name: 🧰 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- 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
- name: ⚙️ Generate Docker image metadata
id: docker_meta
uses: docker/metadata-action@v5
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
@@ -143,19 +43,20 @@ jobs:
org.opencontainers.image.description=Modrinth game metadata query client
org.opencontainers.image.licenses=MIT
- name: Login to GitHub Packages
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
- name: 🔑 Login to GitHub Packages
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
- name: 🔨 Build and push
uses: docker/build-push-action@v6
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 }}
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
+2 -2
View File
@@ -12,10 +12,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
+15 -40
View File
@@ -20,42 +20,28 @@ on:
required: true
type: string
description: 'The environment to deploy to (staging-preview or production-preview)'
head_sha:
required: false
type: string
description: 'The commit SHA to deploy (defaults to github.sha)'
head_ref:
required: false
type: string
description: 'The branch name to deploy (defaults to github.ref_name)'
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: blacksmith-2vcpu-ubuntu-2404
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure environment
id: meta
env:
ENV_INPUT: ${{ inputs.environment }}
REF: ${{ github.ref }}
DEPLOY_SHA: ${{ inputs.head_sha || github.sha }}
run: |
echo "cmd=deploy" >> $GITHUB_OUTPUT
SHA_SHORT="${DEPLOY_SHA::8}"
ENV_INPUT="${{ inputs.environment }}"
REF="${{ github.ref }}"
SHA_SHORT="${GITHUB_SHA::8}"
if [ "$ENV_INPUT" = "staging-preview" ]; then
echo "env=staging" >> $GITHUB_OUTPUT
@@ -77,25 +63,14 @@ jobs:
echo "url=https://modrinth.com" >> $GITHUB_OUTPUT
fi
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4
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-
cache: pnpm
- name: Inject build variables
working-directory: ./apps/frontend
@@ -116,15 +91,15 @@ jobs:
working-directory: ./apps/frontend
run: pnpm build
env:
CF_PAGES_BRANCH: ${{ inputs.head_ref || github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ inputs.head_sha || github.sha }}
CF_PAGES_BRANCH: ${{ github.ref_name }}
CF_PAGES_COMMIT_SHA: ${{ github.sha }}
CF_PAGES_URL: ${{ steps.meta.outputs.url }}
BUILD_ENV: ${{ steps.meta.outputs.env }}
PREVIEW: ${{ inputs.environment != '' && 'true' || 'false' }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: Create Sentry release and upload sourcemaps
uses: getsentry/action-release@5657c9e888b4e2cc85f4d29143ea4131fde4a73a # v3.6.0
uses: getsentry/action-release@v3
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: modrinth
@@ -136,7 +111,7 @@ jobs:
- name: Deploy Cloudflare Worker
id: wrangler
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
@@ -162,7 +137,7 @@ jobs:
- name: Upload deployment URL
if: ${{ inputs.environment != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v6
with:
name: deployment-url-${{ inputs.environment }}
path: deployment-url-${{ inputs.environment }}.txt
+5 -71
View File
@@ -16,89 +16,25 @@ 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 }}
head_sha: ${{ github.event.pull_request.head.sha }}
head_ref: ${{ github.event.pull_request.head.ref }}
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
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: echo "sha_short=${HEAD_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, deploy-storybook]
needs: deploy
steps:
- name: Download deployment URLs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@v7
with:
pattern: deployment-url-*
merge-multiple: true
- name: Read deployment URLs
id: urls
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
STAGING_PREVIEW_URL=$(cat deployment-url-staging-preview.txt)
PRODUCTION_PREVIEW_URL=$(cat deployment-url-production-preview.txt)
@@ -108,11 +44,10 @@ 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-${HEAD_SHA::8}-storybook.modrinth.workers.dev" >> $GITHUB_OUTPUT
- name: Find comment
if: github.event_name == 'pull_request'
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
uses: peter-evans/find-comment@v3
id: fc
with:
token: ${{ secrets.CROWDIN_GH_TOKEN }}
@@ -121,7 +56,7 @@ jobs:
- name: Comment deploy URL on PR
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
uses: peter-evans/create-or-update-comment@v5
with:
token: ${{ secrets.CROWDIN_GH_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
@@ -129,11 +64,10 @@ jobs:
body: |
## Frontend previews
Last deployed commit is [${{ github.event.pull_request.head.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }})
Last deployed commit is [${{ github.sha }}](${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.sha }})
| Environment | URL |
|-------------|-----|
| staging | ${{ steps.urls.outputs.staging-preview-url }} |
| production | ${{ steps.urls.outputs.production-preview-url }} |
| storybook | ${{ steps.urls.outputs.storybook-preview-url }} |
edit-mode: replace
+4 -18
View File
@@ -51,25 +51,14 @@ jobs:
CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4
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@59144859caf016f8b817a2ac9b051578729173c4 # v0.1.2
uses: MarcoIeni/git-config@v0.1
env:
GITHUB_TOKEN: ${{ secrets.CROWDIN_GH_TOKEN }}
@@ -90,7 +79,7 @@ jobs:
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Download translations from Crowdin
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
@@ -106,11 +95,8 @@ 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@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
uses: peter-evans/create-pull-request@v7
with:
title: 'New translations from Crowdin (${{ steps.branch-name.outputs.branch_name }})'
body-path: .github/templates/crowdin-pr.md
+3 -24
View File
@@ -4,14 +4,11 @@ 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:
@@ -56,21 +53,9 @@ jobs:
CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }}
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4
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
@@ -83,7 +68,7 @@ jobs:
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Upload translations to Crowdin
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
uses: crowdin/github-action@v1
with:
upload_sources: true
upload_translations: false
@@ -94,9 +79,3 @@ 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 }}
+21 -127
View File
@@ -3,7 +3,7 @@ name: docker-build
on:
push:
branches:
- 'main'
- '**'
paths:
- .github/workflows/labrinth-docker.yml
- 'apps/labrinth/**'
@@ -19,128 +19,19 @@ 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
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' || '' }}
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 📥 Check out code
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
with:
rustflags: ''
cache: false
- name: 🧰 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Setup mold
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
- name: Install build dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: cmake libcurl4-openssl-dev
version: v2 # cache key
- 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
- name: ⚙️ Generate Docker image metadata
id: docker_meta
uses: docker/metadata-action@v5
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
@@ -158,19 +49,22 @@ jobs:
org.opencontainers.image.description=Modrinth API
org.opencontainers.image.licenses=AGPL-3.0-only
- name: Login to GitHub Packages
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
- name: 🔑 Login to GitHub Packages
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
- name: 🔨 Build and push
uses: docker/build-push-action@v6
with:
context: ./apps/labrinth/docker-stage
file: ./apps/labrinth/Dockerfile
push: true
tags: ${{ steps.docker-meta.outputs.tags }}
labels: ${{ steps.docker-meta.outputs.labels }}
annotations: ${{ steps.docker-meta.outputs.annotations }}
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
-41
View File
@@ -1,41 +0,0 @@
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
+28 -58
View File
@@ -30,71 +30,46 @@ 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: [
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,
]
platform: [macos-latest, windows-latest, ubuntu-latest]
include:
- platform: blacksmith-6vcpu-macos-26
- platform: macos-latest
artifact-target-name: universal-apple-darwin
- platform: blacksmith-8vcpu-windows-2025
- platform: windows-latest
artifact-target-name: x86_64-pc-windows-msvc
- platform: blacksmith-4vcpu-ubuntu-2404
- platform: ubuntu-latest
artifact-target-name: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.platform }}
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ''
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
target: ${{ startsWith(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
- name: Setup sccache
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Enable Corepack
run: corepack enable
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: 'pnpm'
cache: pnpm
- name: Generate tauri-dev.conf.json
shell: bash
@@ -112,28 +87,24 @@ jobs:
EOF
- name: Install Linux build dependencies
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
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
- name: Setup Dasel
uses: jaxxstorm/action-install-gh-release@25e24d2d23ae098373794ef1d6faecb48ee52da8 # v3.0.0
uses: jaxxstorm/action-install-gh-release@v2.1.0
with:
repo: TomWright/dasel
tag: v2.8.1
extension-matching: disable
rename-to: ${{ contains(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
chmod: 0755
- name: Set application version and environment
shell: bash
run: |
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
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
BUILD_ENVIRONMENT="${{ inputs.environment || 'prod' }}"
echo "Setting application version to $APP_VERSION"
echo "Using environment $BUILD_ENVIRONMENT"
@@ -144,13 +115,13 @@ jobs:
cp "packages/app-lib/.env.${BUILD_ENVIRONMENT}" packages/app-lib/.env
- name: Setup Turbo cache
uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14
uses: rharkor/caching-for-turbo@v1.8
- name: Install dependencies
run: pnpm install
- name: Set up Windows code signing
if: contains(matrix.platform, 'windows')
if: startsWith(matrix.platform, 'windows')
shell: bash
run: |
if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then
@@ -160,10 +131,9 @@ jobs:
fi
- name: Build macOS app
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')
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
if: startsWith(matrix.platform, 'macos')
env:
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
@@ -175,8 +145,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') || 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')
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
if: startsWith(matrix.platform, 'ubuntu')
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
@@ -186,9 +156,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') || 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"' }}
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
if: contains(matrix.platform, 'windows')
if: startsWith(matrix.platform, 'windows')
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
@@ -197,7 +167,7 @@ jobs:
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
- name: Upload app bundles
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4
with:
name: App bundle (${{ matrix.artifact-target-name }})
path: |
+80 -17
View File
@@ -4,10 +4,6 @@ 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
@@ -15,7 +11,8 @@ jobs:
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
startsWith(github.event.workflow_run.head_branch, 'v')
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: ubuntu-latest
env:
VERSION_TAG: ${{ github.event.workflow_run.head_branch }}
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
@@ -24,10 +21,10 @@ jobs:
LAUNCHER_FILES_BUCKET_BASE_URL: https://launcher-files.modrinth.com
steps:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 📥 Check out code
uses: actions/checkout@v4
- 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 }}
@@ -46,8 +43,8 @@ jobs:
fi
echo "Verified ${VERSION_TAG} is a tag pointing at ${HEAD_SHA}"
- name: Download Modrinth App artifacts
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
- name: 📥 Download Modrinth App artifacts
uses: dawidd6/action-download-artifact@v11
with:
workflow: theseus-build.yml
workflow_conclusion: success
@@ -55,15 +52,81 @@ 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: |
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
node <<'EOF'
const fs = require('fs');
const version = process.env.VERSION.replace(/^v/, '');
const src = fs.readFileSync('packages/blog/changelog.ts', 'utf8');
- name: Generate version manifest
// Parse every entry in the VERSIONS array, preserving their order
// (which is reverse chronological).
const entryRe = /\{\s*date:\s*`([^`]+)`,\s*product:\s*'(\w+)',(?:\s*version:\s*[`']([^`']+)[`'],)?\s*body:\s*`([\s\S]*?)`,\s*\}/g;
const entries = [];
let match;
while ((match = entryRe.exec(src)) !== null) {
entries.push({
date: match[1],
product: match[2],
version: match[3],
body: match[4],
});
}
const currentIdx = entries.findIndex(
(e) => e.product === 'app' && e.version === version,
);
if (currentIdx === -1) {
console.error(`No app changelog entry found for version ${version}`);
process.exit(1);
}
// Find the surrounding app entries so we can scope hosting changes to
// exactly what shipped between the previous app release and this one.
// Entries are in reverse chronological order, so newer entries have
// smaller indices than older entries.
let newerAppIdx = -1;
for (let i = currentIdx - 1; i >= 0; i--) {
if (entries[i].product === 'app') {
newerAppIdx = i;
break;
}
}
let previousAppIdx = entries.length;
for (let i = currentIdx + 1; i < entries.length; i++) {
if (entries[i].product === 'app') {
previousAppIdx = i;
break;
}
}
const hostingEntries = [];
for (let i = newerAppIdx + 1; i < previousAppIdx; i++) {
if (entries[i].product === 'hosting') {
hostingEntries.push(entries[i]);
}
}
let output = entries[currentIdx].body;
if (hostingEntries.length > 0) {
// Demote any top-level section headings inside hosting bodies so
// they nest cleanly under the "Modrinth Hosting (included)" header.
const demoteHeadings = (body) =>
body.replace(/^(#{1,5})\s/gm, (_, hashes) => `${hashes}# `);
const hostingBody = hostingEntries
.map((e) => demoteHeadings(e.body))
.join('\n\n');
output += `\n\n---\n\n## Modrinth Hosting (included)\n\n${hostingBody}`;
}
fs.writeFileSync('release-notes.md', output);
console.log(`Extracted changelog for app ${version}:`);
console.log(output);
EOF
- name: 🛠️ Generate version manifest
run: |
# Reference: https://tauri.app/plugin/updater/#server-support
jq -nc \
@@ -108,7 +171,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 }}
@@ -143,7 +206,7 @@ jobs:
aws s3 cp updates.json "s3://${AWS_BUCKET}"
- name: Create GitHub release
- name: 🏷️ Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
+30 -132
View File
@@ -8,63 +8,10 @@ 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: ${{ 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' }}
runs-on: ubuntu-latest
env:
# Ensure pnpm output is colored in GitHub Actions logs
@@ -76,107 +23,59 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 📥 Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Install build dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: cmake libcurl4-openssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
version: v2 # cache key
- name: 🧰 Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- name: 🧰 Install pnpm
uses: pnpm/action-setup@v4
- name: 🧰 Setup Node.js
uses: actions/setup-node@v4
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: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
- name: 🧰 Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ''
components: clippy, rustfmt
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 }}-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 --locked cargo-nextest@0.9.133
- name: 🧰 Setup nextest
uses: taiki-e/install-action@nextest
# 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@f9eed3e4680f27610dc6d8c67be1b88593f7dade # v3.0.6
- name: 🧰 Setup cargo-sqlx
uses: taiki-e/cache-cargo-install-action@v2
with:
tool: sqlx-cli@0.8.6
tool: sqlx-cli
locked: false
no-default-features: true
features: rustls,postgres
- name: Setup Turbo cache
uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14
- name: 💨 Setup Turbo cache
uses: rharkor/caching-for-turbo@v1.8
- 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')
@@ -187,22 +86,21 @@ 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
pnpm scripts i18n-icu-contract prune-local --check
git diff --exit-code --color */*/src/locales/*/index.json
git diff --exit-code --color */*/src/locales/en-US/index.json
-6
View File
@@ -22,7 +22,6 @@ node_modules
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/i18n-ally-custom-framework.yml
# IDE - IntelliJ
.idea/*
@@ -82,8 +81,3 @@ 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
-9
View File
@@ -1,11 +1,2 @@
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
+1 -1
View File
@@ -1 +1 @@
24.15.0
20.19.2
+1 -1
View File
@@ -1,3 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode", "Vue.volar", "rust-lang.rust-analyzer", "lokalise.i18n-ally"]
"recommendations": ["esbenp.prettier-vscode", "Vue.volar", "rust-lang.rust-analyzer"]
}
-10
View File
@@ -1,10 +0,0 @@
languageIds:
- vue
- typescript
- javascript
- typescriptreact
usageMatchRegex:
- id:\s*['"]({key})['"]
monopoly: true
+2 -12
View File
@@ -9,7 +9,7 @@
"files.insertFinalNewline": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
"source.organizeImports": "always"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[vue]": {
@@ -31,15 +31,5 @@
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"i18n-ally.localesPaths": [
"packages/ui/src/locales",
"apps/frontend/src/locales",
"packages/moderation/src/locales"
],
"i18n-ally.pathMatcher": "{locale}/index.{ext}",
"i18n-ally.keystyle": "flat",
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.namespace": false,
"i18n-ally.includeSubfolders": true
"scss.lint.unknownAtRules": "ignore"
}
-1
View File
@@ -1 +0,0 @@
CLAUDE.md
+1 -1
View File
@@ -64,7 +64,7 @@ The website and app `prepr` commands
Each project may have its own `CLAUDE.md` with detailed instructions:
- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API
- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API
- [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website
## Code Guidelines
Generated
+54 -510
View File
@@ -27,7 +27,7 @@ checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
dependencies = [
"actix-utils",
"actix-web",
"derive_more 2.1.1",
"derive_more 2.0.1",
"futures-util",
"log",
"once_cell",
@@ -46,7 +46,7 @@ dependencies = [
"actix-web",
"bitflags 2.9.4",
"bytes",
"derive_more 2.1.1",
"derive_more 2.0.1",
"futures-core",
"http-range",
"log",
@@ -72,10 +72,10 @@ dependencies = [
"brotli",
"bytes",
"bytestring",
"derive_more 2.1.1",
"derive_more 2.0.1",
"encoding_rs",
"flate2",
"foldhash 0.1.5",
"foldhash",
"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.1.1",
"derive_more 2.0.1",
"encoding_rs",
"foldhash 0.1.5",
"foldhash",
"futures-core",
"futures-util",
"impl-more",
@@ -914,18 +914,6 @@ 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"
@@ -976,295 +964,6 @@ 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"
@@ -1353,16 +1052,6 @@ 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"
@@ -1683,16 +1372,6 @@ 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"
@@ -1893,19 +1572,18 @@ dependencies = [
[[package]]
name = "cidre"
version = "0.15.0"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c903ff1729987d29e07295d2c5841993db25ff86cca43bee04505878d3ec1a7"
checksum = "0cc0eb4d7faf9f94493eaf7e7c0534dee2dfa9642353382039572abb4e0e56e9"
dependencies = [
"cc",
"cidre-macros",
]
[[package]]
name = "cidre-macros"
version = "0.5.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6facfeffa8b9792dc014bb4e65e364d13518b74e06783d56249810a3e48cfad7"
checksum = "82bc2f84c0baaa09299da3a03864491549685912c1e338a54211e00589dc1e4c"
[[package]]
name = "cityhash-rs"
@@ -2224,15 +1902,6 @@ 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"
@@ -2347,18 +2016,6 @@ 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"
@@ -2793,23 +2450,21 @@ dependencies = [
[[package]]
name = "derive_more"
version = "2.1.1"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.1"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"convert_case 0.10.0",
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.106",
"unicode-xid",
]
@@ -3059,6 +2714,29 @@ 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"
@@ -3461,7 +3139,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"spin 0.9.8",
"spin",
]
[[package]]
@@ -3476,12 +3154,6 @@ 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"
@@ -4133,7 +3805,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.1.5",
"foldhash",
]
[[package]]
@@ -4141,11 +3813,6 @@ 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"
@@ -4914,9 +4581,9 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.12"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags 2.9.4",
"cfg-if",
@@ -5235,7 +4902,6 @@ dependencies = [
"async-minecraft-ping",
"async-stripe",
"async-trait",
"aws-sdk-s3",
"base64 0.22.1",
"bitflags 2.9.4",
"bytes",
@@ -5248,10 +4914,11 @@ dependencies = [
"const_format",
"dashmap",
"deadpool-redis",
"derive_more 2.1.1",
"derive_more 2.0.1",
"dotenv-build",
"dotenvy",
"either",
"elasticsearch",
"eyre",
"futures",
"futures-util",
@@ -5274,13 +4941,12 @@ dependencies = [
"paste",
"path-util",
"prometheus",
"quick-xml 0.38.3",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rdkafka",
"redis",
"regex",
"reqwest 0.12.24",
"rust-s3",
"rust_decimal",
"rust_iso3166",
"rustls 0.23.32",
@@ -5339,7 +5005,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin 0.9.8",
"spin",
]
[[package]]
@@ -5490,18 +5156,6 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "libz-sys"
version = "1.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -5573,15 +5227,6 @@ 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"
@@ -5870,7 +5515,7 @@ name = "modrinth-util"
version = "0.0.0"
dependencies = [
"actix-web",
"derive_more 2.1.1",
"derive_more 2.0.1",
"dotenvy",
"eyre",
"modrinth-log",
@@ -5936,7 +5581,7 @@ dependencies = [
"arc-swap",
"bytes",
"chrono",
"derive_more 2.1.1",
"derive_more 2.0.1",
"reqwest 0.12.24",
"rust_decimal",
"rust_iso3166",
@@ -6823,12 +6468,6 @@ 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"
@@ -6953,7 +6592,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
name = "path-util"
version = "0.0.0"
dependencies = [
"derive_more 2.1.1",
"derive_more 2.0.1",
"itertools 0.14.0",
"serde",
"typed-path",
@@ -7980,37 +7619,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rdkafka"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1beea247b9a7600a81d4cc33f659ce1a77e1988323d7d2809c7ed1c21f4c316d"
dependencies = [
"futures-channel",
"futures-util",
"libc",
"log",
"rdkafka-sys",
"serde",
"serde_derive",
"serde_json",
"slab",
"tokio",
]
[[package]]
name = "rdkafka-sys"
version = "4.10.0+2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e234cf318915c1059d4921ef7f75616b5219b10b46e9f3a511a15eb4b56a3f77"
dependencies = [
"cmake",
"libc",
"libz-sys",
"num_enum",
"pkg-config",
]
[[package]]
name = "redis"
version = "0.32.7"
@@ -8203,20 +7811,15 @@ 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",
@@ -8558,33 +8161,6 @@ 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"
@@ -9483,12 +9059,6 @@ 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"
@@ -9704,7 +9274,7 @@ dependencies = [
name = "sqlx-tracing"
version = "0.2.0"
dependencies = [
"derive_more 2.1.1",
"derive_more 2.0.1",
"futures",
"opentelemetry",
"opentelemetry-testing",
@@ -10339,8 +9909,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
source = "git+https://github.com/modrinth/plugins-workspace?rev=0d30f2aa28ec668ce187d527da1c475da3c01cbc#0d30f2aa28ec668ce187d527da1c475da3c01cbc"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
dependencies = [
"base64 0.22.1",
"dirs",
@@ -10352,8 +9923,7 @@ dependencies = [
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls 0.23.32",
"reqwest 0.12.24",
"semver",
"serde",
"serde_json",
@@ -10577,7 +10147,7 @@ dependencies = [
"daedalus",
"dashmap",
"data-url",
"derive_more 2.1.1",
"derive_more 2.0.1",
"dirs",
"discord-rich-presence",
"dotenvy",
@@ -10589,7 +10159,6 @@ dependencies = [
"flate2",
"fs4",
"futures",
"futures-lite 2.6.1",
"heck 0.5.0",
"hickory-resolver 0.25.2",
"indicatif",
@@ -10641,8 +10210,6 @@ version = "1.0.0-local"
dependencies = [
"async_zip",
"chrono",
"core-foundation 0.10.1",
"core-graphics",
"daedalus",
"dashmap",
"either",
@@ -10650,7 +10217,6 @@ dependencies = [
"hyper 1.7.0",
"hyper-util",
"native-dialog",
"objc2-app-kit",
"paste",
"path-util",
"serde",
@@ -10676,7 +10242,6 @@ dependencies = [
"urlencoding",
"uuid 1.18.1",
"webview2-com",
"windows",
"windows-core 0.61.2",
]
@@ -11727,12 +11292,6 @@ 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"
@@ -12032,15 +11591,6 @@ 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"
@@ -12808,12 +12358,6 @@ 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"
+5 -17
View File
@@ -44,11 +44,6 @@ 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"
@@ -56,7 +51,7 @@ bytes = "1.10.1"
censor = "0.3.0"
chardetng = "0.1.17"
chrono = "0.4.42"
cidre = { version = "0.15.0", default-features = false, features = [
cidre = { version = "0.11.3", default-features = false, features = [
"macos_15_0"
] }
clap = "4.5.48"
@@ -64,14 +59,12 @@ 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.1.1"
derive_more = "2.0.1"
directories = "6.0.0"
dirs = "6.0.0"
discord-rich-presence = "1.0.0"
@@ -79,13 +72,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"
@@ -126,7 +119,6 @@ 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"
@@ -140,7 +132,6 @@ quick-xml = "0.38.3"
quote = { version = "1.0" }
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
rdkafka = { version = "0.36.2", features = ["cmake-build"] }
redis = "0.32.7"
regex = "1.12.2"
reqwest = { version = "0.12.24", default-features = false }
@@ -193,7 +184,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 = { git = "https://github.com/modrinth/plugins-workspace", rev = "0d30f2aa28ec668ce187d527da1c475da3c01cbc", default-features = false, features = [
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
"rustls-tls",
"zip",
] }
@@ -267,10 +258,8 @@ 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"
type_complexity = "allow"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
wildcard_dependencies = "warn"
@@ -284,11 +273,10 @@ 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
+3 -3
View File
@@ -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 guides:
This repository contains two primary packages. For detailed development information, please refer to their respective READMEs:
- [Website frontend](https://docs.modrinth.com/contributing/knossos/)
- [Desktop app](https://docs.modrinth.com/contributing/theseus/)
- [Web Interface](apps/frontend/README.md)
- [Desktop App](apps/app/README.md)
## Contributing
+2 -4
View File
@@ -10,7 +10,6 @@
"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": {
@@ -20,7 +19,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",
@@ -44,8 +43,7 @@
"vue": "^3.5.13",
"vue-i18n": "^10.0.0",
"vue-router": "^4.6.0",
"vue-virtual-scroller": "v2.0.0-beta.8",
"vuedraggable": "^4.1.0"
"vue-virtual-scroller": "v2.0.0-beta.8"
},
"devDependencies": {
"@eslint/compat": "^1.1.1",
+148 -439
View File
@@ -1,7 +1,6 @@
<script setup>
import {
AuthFeature,
ModrinthApiError,
NodeAuthFeature,
nodeAuthState,
PanelVersionFeature,
@@ -12,6 +11,7 @@ import {
ArrowBigUpDashIcon,
ChangeSkinIcon,
CompassIcon,
DownloadIcon,
ExternalIcon,
HomeIcon,
LeftArrowIcon,
@@ -35,7 +35,6 @@ import {
ButtonStyled,
commonMessages,
ContentInstallModal,
ContentUpdaterModal,
CreationFlowModal,
defineMessages,
I18nDebugPanel,
@@ -44,17 +43,16 @@ import {
NotificationPanel,
OverflowMenu,
PopupNotificationPanel,
ProgressSpinner,
provideModalBehavior,
provideModrinthClient,
provideNotificationManager,
providePageContext,
providePopupNotificationManager,
useDebugLogger,
useFormatBytes,
useHostingIntercom,
useVIntl,
} from '@modrinth/ui'
import { renderString } from '@modrinth/utils'
import { formatBytes, renderString } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { getVersion } from '@tauri-apps/api/app'
import { invoke } from '@tauri-apps/api/core'
@@ -69,12 +67,11 @@ import { RouterView, useRoute, useRouter } from 'vue-router'
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
import AccountsCard from '@/components/ui/AccountsCard.vue'
import AppActionBar from '@/components/ui/AppActionBar.vue'
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
import ErrorModal from '@/components/ui/ErrorModal.vue'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import AddServerToInstanceModal from '@/components/ui/install_flow/AddServerToInstanceModal.vue'
import UnknownPackWarningModal from '@/components/ui/install_flow/UnknownPackWarningModal.vue'
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
import MinecraftAuthErrorModal from '@/components/ui/minecraft-auth-error-modal/MinecraftAuthErrorModal.vue'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
@@ -82,9 +79,9 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
import NavButton from '@/components/ui/NavButton.vue'
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import WindowControls from '@/components/ui/WindowControls.vue'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
@@ -93,14 +90,12 @@ import { hide_ads_window, init_ads_window, show_ads_window } from '@/helpers/ads
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
import { check_reachable } from '@/helpers/auth.js'
import { get_user, get_version } from '@/helpers/cache.js'
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
import { create_profile_and_install_from_file } from '@/helpers/pack'
import { list } from '@/helpers/profile.js'
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
import { get_opening_command, initialize_state } from '@/helpers/state'
import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts'
import {
areUpdatesEnabled,
enqueueUpdateForInstallation,
@@ -108,19 +103,8 @@ import {
getUpdateSize,
isDev,
isNetworkMetered,
setRestartAfterPendingUpdate,
} from '@/helpers/utils.js'
import i18n from '@/i18n.config'
import {
appUpdateState,
downloadAvailableAppUpdate,
getNextAppUpdatePopupTime,
installAvailableAppUpdate,
markAppUpdateActionable,
markAppUpdatePopupShown,
openAppUpdateChangelog,
setAppUpdateActions,
} from '@/providers/app-update.ts'
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
import {
provideAppUpdateDownloadProgress,
@@ -139,42 +123,6 @@ import { AppNotificationManager } from './providers/app-notifications'
import { AppPopupNotificationManager } from './providers/app-popup-notifications'
const themeStore = useTheming()
const router = useRouter()
const route = useRoute()
const APP_LEFT_NAV_WIDTH = '4rem'
const APP_SIDEBAR_WIDTH = 300
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
const credentials = ref()
const sidebarToggled = ref(true)
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
const prideFundraiserEnabled = computed(
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
)
const hostingIntercomIdentityKey = computed(() => {
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
const userId = credentials.value?.user_id ?? credentials.value?.user?.id ?? 'anonymous'
return `${userId}:${serverId ?? 'hosting'}`
})
const hostingIntercom = useHostingIntercom({
enabled: computed(() => hostingRouteActive.value && !!credentials.value?.session),
appId: 'ykeritl9',
fetchToken: fetchIntercomToken,
identityKey: hostingIntercomIdentityKey,
horizontalPadding: computed(() =>
sidebarVisible.value
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
: INTERCOM_BUBBLE_DEFAULT_PADDING,
),
})
const notificationManager = new AppNotificationManager()
provideNotificationManager(notificationManager)
@@ -184,9 +132,8 @@ const popupNotificationManager = new AppPopupNotificationManager()
providePopupNotificationManager(popupNotificationManager)
const { addPopupNotification } = popupNotificationManager
const appVersion = getVersion()
const tauriApiClient = new TauriModrinthClient({
userAgent: async () => `modrinth/theseus/${await appVersion} (support@modrinth.com)`,
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
labrinthBaseUrl: config.labrinthBaseUrl,
archonBaseUrl: config.archonBaseUrl,
features: [
@@ -206,20 +153,9 @@ const tauriApiClient = new TauriModrinthClient({
],
})
provideModrinthClient(tauriApiClient)
const { data: authenticatedModrinthUser } = useQuery({
queryKey: computed(() => ['authenticated-user', 'campaigns', credentials.value?.user?.id]),
queryFn: () => tauriApiClient.labrinth.users_v3.getAuthenticated(),
enabled: () => !!credentials.value?.session,
retry: false,
})
providePageContext({
hierarchicalSidebarAvailable: ref(true),
showAds: ref(false),
floatingActionBarOffsets: {
left: ref(APP_LEFT_NAV_WIDTH),
right: computed(() => (sidebarVisible.value ? `${APP_SIDEBAR_WIDTH}px` : '0px')),
},
intercomBubble: hostingIntercom.intercomBubble,
featureFlags: {
serverRamAsBytesAlwaysOn: computed(() =>
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
@@ -235,21 +171,18 @@ provideModalBehavior({
const {
installationModal,
unknownPackWarningModal,
fetchExistingInstanceNames,
handleCreate,
handleBrowseModpacks,
searchModpacks,
getProjectVersions,
getLoaderManifest,
setModpackAlreadyInstalledModal,
handleModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance,
} = setupProviders(notificationManager, popupNotificationManager)
} = setupProviders(notificationManager)
const news = ref([])
const availableSurvey = ref(false)
const displayedServerInviteNotifications = new Set()
const offline = ref(!navigator.onLine)
window.addEventListener('offline', () => {
@@ -304,15 +237,11 @@ onMounted(async () => {
onUnmounted(async () => {
document.querySelector('body').removeEventListener('click', handleClick)
document.querySelector('body').removeEventListener('auxclick', handleAuxClick)
unsubscribeSidebarToggle()
clearDelayedUpdatePopup()
await unlistenUpdateDownload?.()
})
const { formatMessage } = useVIntl()
const formatBytes = useFormatBytes()
const messages = defineMessages({
updateInstalledToastTitle: {
id: 'app.update.complete-toast.title',
@@ -322,6 +251,18 @@ const messages = defineMessages({
id: 'app.update.complete-toast.text',
defaultMessage: 'Click here to view the changelog.',
},
reloadToUpdate: {
id: 'app.update.reload-to-update',
defaultMessage: 'Reload to install update',
},
downloadUpdate: {
id: 'app.update.download-update',
defaultMessage: 'Download update',
},
downloadingUpdate: {
id: 'app.update.downloading-update',
defaultMessage: 'Downloading update ({percent}%)',
},
authUnreachableHeader: {
id: 'app.auth-servers.unreachable.header',
defaultMessage: 'Cannot reach authentication servers',
@@ -340,7 +281,6 @@ async function setupApp() {
locale,
telemetry,
collapsed_navigation,
hide_nametag_skins_page,
advanced_rendering,
onboarded,
default_page,
@@ -371,7 +311,6 @@ async function setupApp() {
themeStore.setThemeState(theme)
themeStore.collapsedNavigation = collapsed_navigation
themeStore.advancedRendering = advanced_rendering
themeStore.hideNametagSkinsPage = hide_nametag_skins_page
themeStore.toggleSidebar = toggle_sidebar
themeStore.devMode = developer_mode
themeStore.featureFlags = feature_flags
@@ -479,6 +418,9 @@ const handleClose = async () => {
await getCurrentWindow().close()
}
const router = useRouter()
const route = useRoute()
const loading = setupLoadingStateProvider()
loading.setEnabled(false)
let initialLoadToken = loading.begin()
@@ -609,16 +551,6 @@ const {
handleModpackDuplicateCreateAnyway: handleContentInstallModpackDuplicateCreateAnyway,
handleModpackDuplicateGoToInstance: handleContentInstallModpackDuplicateGoToInstance,
setIncompatibilityWarningModal: setContentIncompatibilityWarningModal,
incompatibilityWarningVersions: contentInstallIncompatibilityWarningVersions,
incompatibilityWarningCurrentGameVersion: contentInstallIncompatibilityWarningCurrentGameVersion,
incompatibilityWarningCurrentLoader: contentInstallIncompatibilityWarningCurrentLoader,
incompatibilityWarningProjectType: contentInstallIncompatibilityWarningProjectType,
incompatibilityWarningProjectIconUrl: contentInstallIncompatibilityWarningProjectIconUrl,
incompatibilityWarningProjectName: contentInstallIncompatibilityWarningProjectName,
incompatibilityWarningMessage: contentInstallIncompatibilityWarningMessage,
incompatibilityWarningInstalling: contentInstallIncompatibilityWarningInstalling,
handleIncompatibilityWarningInstall: handleContentInstallIncompatibilityWarningInstall,
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
@@ -638,13 +570,9 @@ const incompatibilityWarningModal = ref()
const installToPlayModal = ref()
const updateToPlayModal = ref()
const modrinthLoginFlowWaitModal = ref()
const credentials = ref()
watch(incompatibilityWarningModal, (modal) => {
if (modal) {
setContentIncompatibilityWarningModal(modal)
}
})
const modrinthLoginFlowWaitModal = ref()
setupAuthProvider(credentials, async (_redirectPath) => {
await signIn()
@@ -702,43 +630,28 @@ async function logOut() {
await fetchCredentials()
}
const MIDAS_BITFLAG = 1 << 0
const hasPlus = computed(
() =>
!!credentials.value?.user &&
(hasMidasBadge(credentials.value.user) ||
hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)),
credentials.value &&
credentials.value.user &&
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
)
const sidebarToggled = ref(true)
themeStore.$subscribe(() => {
sidebarToggled.value = !themeStore.toggleSidebar
})
const forceSidebar = computed(
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
)
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
const showAd = computed(
() => sidebarVisible.value && !hasPlus.value && credentials.value !== undefined,
)
async function fetchIntercomToken() {
const creds = await getCreds()
if (!creds?.session) {
throw new Error('Not authenticated')
}
const params = new URLSearchParams()
const rawServerId = route.params.id
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
if (route.path.startsWith('/hosting/manage/') && typeof serverId === 'string') {
params.set('server_id', serverId)
}
const query = params.size > 0 ? `?${params.toString()}` : ''
const response = await tauriFetch(`${config.siteUrl}/api/intercom/messenger-jwt${query}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${creds.session}`,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch Intercom token: ${response.status}`)
}
return await response.json()
}
watch(showAd, () => {
if (!showAd.value) {
hide_ads_window(true)
@@ -766,95 +679,13 @@ const accounts = ref(null)
provide('accountsCard', accounts)
command_listener(handleCommand)
notification_listener(handleLiveNotification)
async function markLiveNotificationRead(notification) {
try {
await tauriApiClient.labrinth.notifications_v2.markAsRead(notification.id)
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 404) {
console.warn(`notification ${notification.id} could not be marked as read`, error)
return
}
throw error
}
}
async function respondToServerInvite(notification, action) {
const serverId = notification.body?.server_id
if (typeof serverId !== 'string') {
throw new Error('Missing server ID for invite notification.')
}
await tauriApiClient.request(`/servers/${serverId}/invites/${action}`, {
api: 'archon',
version: 1,
method: 'POST',
})
await markLiveNotificationRead(notification)
return serverId
}
async function acceptServerInviteNotification(notification) {
try {
const serverId = await respondToServerInvite(notification, 'accept')
await router.push(`/hosting/manage/${encodeURIComponent(serverId)}`)
queryClient.invalidateQueries({ queryKey: ['servers'] })
} catch (error) {
handleError(error)
}
}
async function declineServerInviteNotification(notification) {
try {
await respondToServerInvite(notification, 'decline')
} catch (error) {
handleError(error)
}
}
function openServerInviteInviterProfile(inviterName) {
if (!inviterName) return
openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`)
}
async function handleLiveNotification(notification) {
if (notification?.body?.type !== 'server_invite' || notification.read) return
if (displayedServerInviteNotifications.has(notification.id)) return
displayedServerInviteNotifications.add(notification.id)
const serverName =
typeof notification.body.server_name === 'string' ? notification.body.server_name : 'a server'
const inviterId = notification.body.invited_by
const invitedBy =
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
addPopupNotification({
title: serverName,
autoCloseMs: null,
toast: {
type: 'server-invite',
actorName: invitedBy?.username ?? null,
actorAvatarUrl: invitedBy?.avatar_url ?? null,
entityName: serverName,
onAccept: () => acceptServerInviteNotification(notification),
onDecline: () => declineServerInviteNotification(notification),
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
},
})
}
async function handleCommand(e) {
if (!e) return
if (e.event === 'RunMRPack') {
// RunMRPack should directly install a local mrpack given a path
if (e.path.endsWith('.mrpack')) {
await create_profile_and_install_from_file(e.path, (createProfile, fileName) =>
unknownPackWarningModal.value?.show(createProfile, fileName),
).catch(handleError)
await create_profile_and_install_from_file(e.path).catch(handleError)
trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop',
})
@@ -879,21 +710,20 @@ async function handleCommand(e) {
}
const appUpdateDownload = {
progress: appUpdateState.progress,
progress: ref(0),
version: ref(),
}
let unlistenUpdateDownload
const {
metered,
finishedDownloading,
downloading,
restarting,
availableUpdate,
updateSize,
updatesEnabled,
} = appUpdateState
let delayedUpdatePopupTimeout = null
const downloadProgress = computed(() => appUpdateDownload.progress.value)
const downloadPercent = computed(() => Math.trunc(appUpdateDownload.progress.value * 100))
const metered = ref(true)
const finishedDownloading = ref(false)
const restarting = ref(false)
const availableUpdate = ref(null)
const updateSize = ref(null)
const updatesEnabled = ref(true)
const updatePopupMessages = defineMessages({
updateAvailable: {
@@ -904,6 +734,11 @@ const updatePopupMessages = defineMessages({
id: 'app.update-popup.download-complete',
defaultMessage: 'Download complete',
},
body: {
id: 'app.update-popup.body',
defaultMessage:
'Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App.',
},
meteredBody: {
id: 'app.update-popup.body.metered',
defaultMessage: `Modrinth App v{version} is available now! Since you're on a metered network, we didn't automatically download it.`,
@@ -919,7 +754,7 @@ const updatePopupMessages = defineMessages({
},
reload: {
id: 'app.update-popup.reload',
defaultMessage: 'Reload to update',
defaultMessage: 'Reload',
},
download: {
id: 'app.update-popup.download',
@@ -931,106 +766,6 @@ const updatePopupMessages = defineMessages({
},
})
function clearDelayedUpdatePopup() {
if (delayedUpdatePopupTimeout !== null) {
clearTimeout(delayedUpdatePopupTimeout)
delayedUpdatePopupTimeout = null
}
}
function getCurrentUpdatePromptStage() {
return finishedDownloading.value ? 'downloaded' : 'available'
}
function scheduleDelayedUpdatePopup() {
clearDelayedUpdatePopup()
const version = availableUpdate.value?.version
if (!version) {
return
}
const nextPopupTime = getNextAppUpdatePopupTime(version, getCurrentUpdatePromptStage())
if (nextPopupTime === null) {
return
}
const delay = nextPopupTime - Date.now()
if (delay <= 0) {
showDelayedUpdatePopup()
return
}
delayedUpdatePopupTimeout = setTimeout(showDelayedUpdatePopup, Math.min(delay, 2_147_483_647))
}
function showDelayedUpdatePopup() {
const update = availableUpdate.value
if (!update) {
return
}
const stage = getCurrentUpdatePromptStage()
const nextPopupTime = getNextAppUpdatePopupTime(update.version, stage)
if (nextPopupTime === null) {
return
}
if (Date.now() < nextPopupTime) {
scheduleDelayedUpdatePopup()
return
}
if (metered.value && !finishedDownloading.value) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.download, {
size: formatBytes(updateSize.value ?? 0),
}),
action: () => downloadAvailableAppUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openAppUpdateChangelog(),
keepOpen: true,
},
],
})
} else if (finishedDownloading.value) {
addPopupNotification({
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: update.version,
}),
type: 'success',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.reload),
action: () => installAvailableAppUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openAppUpdateChangelog(),
keepOpen: true,
},
],
})
} else {
scheduleDelayedUpdatePopup()
return
}
markAppUpdatePopupShown(update.version, stage)
}
async function checkUpdates() {
if (!(await areUpdatesEnabled())) {
console.log('Skipping update check as updates are disabled in this build or environment')
@@ -1054,15 +789,11 @@ async function checkUpdates() {
if (isExistingUpdate) {
console.log('Update is already known')
scheduleDelayedUpdatePopup()
return
}
appUpdateDownload.progress.value = 0
finishedDownloading.value = false
downloading.value = false
updateSize.value = null
availableUpdate.value = update
console.log(`Update ${update.version} is available.`)
@@ -1072,11 +803,33 @@ async function checkUpdates() {
downloadUpdate(update)
} else {
console.log(`Metered connection detected, not auto-downloading update.`)
markAppUpdateActionable(update.version)
scheduleDelayedUpdatePopup()
getUpdateSize(update.rid).then((size) => {
updateSize.value = size
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.meteredBody, { version: update.version }),
type: 'info',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.download, {
size: formatBytes(updateSize.value ?? 0),
}),
action: () => downloadAvailableUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
},
],
})
})
}
getUpdateSize(update.rid).then((size) => (updateSize.value = size))
availableUpdate.value = update
}
await performCheck()
@@ -1098,17 +851,12 @@ async function checkLinuxUpdates() {
const latestVersion = updates?.version
if (latestVersion && latestVersion !== currentVersion) {
markAppUpdateActionable(latestVersion)
const nextPopupTime = getNextAppUpdatePopupTime(latestVersion)
if (nextPopupTime !== null && Date.now() >= nextPopupTime) {
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
type: 'info',
autoCloseMs: null,
})
markAppUpdatePopupShown(latestVersion)
}
addPopupNotification({
title: formatMessage(updatePopupMessages.updateAvailable),
text: formatMessage(updatePopupMessages.linuxBody, { version: latestVersion }),
type: 'info',
autoCloseMs: null,
})
}
} catch (e) {
console.error('Failed to check for updates:', e)
@@ -1122,88 +870,59 @@ async function downloadAvailableUpdate() {
async function downloadUpdate(versionToDownload) {
if (!versionToDownload) {
handleError(`Failed to download update: no version available`)
return
}
if (downloading.value || appUpdateDownload.progress.value !== 0) {
if (appUpdateDownload.progress.value !== 0) {
console.error(`Update ${versionToDownload.version} already downloading`)
return
}
console.log(`Downloading update ${versionToDownload.version}`)
downloading.value = true
try {
enqueueUpdateForInstallation(versionToDownload.rid)
.then(() => {
downloading.value = false
finishedDownloading.value = true
unlistenUpdateDownload?.().then(() => {
unlistenUpdateDownload = null
})
console.log('Finished downloading!')
markAppUpdateActionable(versionToDownload.version, 'downloaded')
scheduleDelayedUpdatePopup()
enqueueUpdateForInstallation(versionToDownload.rid).then(() => {
finishedDownloading.value = true
unlistenUpdateDownload?.().then(() => {
unlistenUpdateDownload = null
})
.catch((e) => {
downloading.value = false
appUpdateDownload.progress.value = 0
handleError(e)
console.log('Finished downloading!')
addPopupNotification({
title: formatMessage(updatePopupMessages.downloadComplete),
text: formatMessage(updatePopupMessages.downloadedBody, {
version: versionToDownload.version,
}),
type: 'success',
autoCloseMs: null,
buttons: [
{
label: formatMessage(updatePopupMessages.reload),
action: () => installUpdate(),
color: 'brand',
},
{
label: formatMessage(updatePopupMessages.changelog),
action: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
},
],
})
})
unlistenUpdateDownload = await subscribeToDownloadProgress(
appUpdateDownload,
versionToDownload.version,
)
} catch (e) {
downloading.value = false
appUpdateDownload.progress.value = 0
handleError(e)
}
}
async function installUpdate() {
restarting.value = true
try {
await setRestartAfterPendingUpdate(true)
} catch (e) {
restarting.value = false
handleError(e)
return
}
setTimeout(async () => {
await handleClose()
}, 250)
}
setAppUpdateActions({
download: downloadAvailableUpdate,
install: installUpdate,
changelog: () => openUrl('https://modrinth.com/news/changelog?filter=app'),
})
async function openModrinthProjectLinkInApp(parsed) {
const { slug, pathSuffix, url } = parsed
const loadToken = loading.begin()
try {
const { id } = await tauriApiClient.labrinth.projects_v2.check(slug)
const query = mergeUrlQuery(route.query, url)
await router.push({
path: `/project/${id}${pathSuffix}`,
query,
hash: url.hash || undefined,
})
} catch (err) {
if (err instanceof ModrinthApiError && err.statusCode === 404) {
openUrl(url.href)
} else {
handleError(err)
}
} finally {
loading.end(loadToken)
}
}
function handleClick(e) {
let target = e.target
while (target != null) {
@@ -1216,12 +935,7 @@ function handleClick(e) {
!target.href.startsWith('https://tauri.localhost') &&
!target.href.startsWith('http://tauri.localhost')
) {
const parsed = parseModrinthLink(target.href)
if (target.target !== '_blank' && parsed) {
void openModrinthProjectLinkInApp(parsed)
} else {
openUrl(target.href)
}
openUrl(target.href)
}
e.preventDefault()
break
@@ -1358,11 +1072,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
</script>
<template>
<WindowControls />
<SplashScreen v-if="!stateFailed" ref="splashScreen" data-tauri-drag-region />
<div id="teleports"></div>
<div
v-if="stateInitialized"
class="app-grid-layout relative"
class="app-grid-layout experimental-styles-within relative"
:class="{ 'disable-advanced-rendering': !themeStore.advancedRendering }"
>
<Transition name="fade">
@@ -1393,11 +1108,9 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
:fetch-existing-instance-names="fetchExistingInstanceNames"
:search-modpacks="searchModpacks"
:get-project-versions="getProjectVersions"
:get-loader-manifest="getLoaderManifest"
@create="handleCreate"
@browse-modpacks="handleBrowseModpacks"
/>
<UnknownPackWarningModal ref="unknownPackWarningModal" />
<div
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
>
@@ -1415,7 +1128,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
>
<CompassIcon />
</NavButton>
<NavButton v-tooltip.right="'Skin selector'" to="/skins">
<NavButton v-tooltip.right="'Skins (Beta)'" to="/skins">
<ChangeSkinIcon />
</NavButton>
<NavButton
@@ -1451,6 +1164,33 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<PlusIcon />
</NavButton>
<div class="flex flex-grow"></div>
<Transition name="nav-button-animated">
<div v-if="availableUpdate && !restarting && (finishedDownloading || metered)">
<NavButton
v-tooltip.right="
formatMessage(
finishedDownloading
? messages.reloadToUpdate
: downloadProgress === 0
? messages.downloadUpdate
: messages.downloadingUpdate,
{
percent: downloadPercent,
},
)
"
:to="finishedDownloading ? installUpdate : downloadAvailableUpdate"
>
<ProgressSpinner
v-if="downloadProgress > 0 && downloadProgress < 1"
class="text-brand"
:progress="downloadProgress"
/>
<RefreshCwIcon v-else-if="finishedDownloading" class="text-brand" />
<DownloadIcon v-else class="text-brand" />
</NavButton>
</div>
</Transition>
<NavButton
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
:to="() => $refs.settingsModal.show()"
@@ -1527,16 +1267,15 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
</ButtonStyled>
<div class="flex mr-3">
<Suspense>
<AppActionBar />
<RunningAppBar />
</Suspense>
</div>
<WindowControls />
</section>
</div>
</div>
<div
v-if="stateInitialized"
class="app-contents"
class="app-contents experimental-styles-within"
:class="{
'sidebar-enabled': sidebarVisible,
'disable-advanced-rendering': !themeStore.advancedRendering,
@@ -1630,7 +1369,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
<h3 class="text-base text-primary font-medium m-0">Playing as</h3>
<suspense>
<AccountsCard ref="accounts" />
<AccountsCard ref="accounts" mode="small" />
</suspense>
</div>
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
@@ -1638,10 +1377,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
</suspense>
</div>
<PrideFundraiserBanner
v-if="prideFundraiserEnabled"
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
/>
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
<div class="space-y-4 flex flex-col items-center w-full">
@@ -1698,22 +1433,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
@go-to-instance="handleModpackDuplicateGoToInstance"
/>
<AddServerToInstanceModal ref="addServerToInstanceModal" />
<ContentUpdaterModal
ref="incompatibilityWarningModal"
mode="incompatibility-warning"
:versions="contentInstallIncompatibilityWarningVersions"
:current-game-version="contentInstallIncompatibilityWarningCurrentGameVersion"
:current-loader="contentInstallIncompatibilityWarningCurrentLoader"
current-version-id=""
:is-app="true"
:project-type="contentInstallIncompatibilityWarningProjectType"
:project-icon-url="contentInstallIncompatibilityWarningProjectIconUrl"
:project-name="contentInstallIncompatibilityWarningProjectName"
:warning="contentInstallIncompatibilityWarningMessage"
:action-loading="contentInstallIncompatibilityWarningInstalling"
@update="handleContentInstallIncompatibilityWarningInstall"
@cancel="handleContentInstallIncompatibilityWarningCancel"
/>
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
<ModpackAlreadyInstalledModal
ref="contentInstallModpackAlreadyInstalledModal"
@create-anyway="handleContentInstallModpackDuplicateCreateAnyway"
@@ -1744,15 +1464,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
.app-grid-navbar {
grid-area: nav;
position: relative;
z-index: 2;
}
.app-grid-statusbar {
grid-area: status;
padding-right: var(--window-controls-width, 0px);
position: relative;
z-index: 2;
}
[data-tauri-drag-region-exclude] {
@@ -1820,12 +1536,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
&.app-contents::before {
box-shadow: none;
}
*,
:deep(*) {
box-shadow: none !important;
--tw-drop-shadow:;
}
}
.app-sidebar::before {
@@ -1844,11 +1554,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
height: 100%;
overflow: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
}
.app-contents::before {
z-index: 30;
z-index: 1;
content: '';
position: fixed;
left: var(--left-bar-width);
@@ -180,6 +180,13 @@ img {
}
}
button,
input[type='button'] {
cursor: pointer;
border: none;
outline: 2px solid transparent;
}
@import '@modrinth/assets/omorphia.scss';
input {
@@ -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,107 +1,81 @@
<template>
<div
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"
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"
>
<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>
<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>
</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>
<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>
</Accordion>
<DropdownIcon class="w-5 h-5 shrink-0" />
</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>
</div>
</div>
<Button v-if="accounts.length > 0" @click="login()">
<PlusIcon />
Add account
</Button>
</Card>
</transition>
</template>
<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'
<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'
import { trackEvent } from '@/helpers/analytics'
import {
@@ -113,47 +87,39 @@ 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()
const emit = defineEmits<{
change: []
}>()
defineProps({
mode: {
type: String,
required: true,
default: 'normal',
},
})
type MinecraftCredential = {
profile: {
id: string
name: string
}
}
const emit = defineEmits(['change'])
const accounts: Ref<MinecraftCredential[]> = ref([])
const accounts = ref({})
const loginDisabled = ref(false)
const defaultUser = ref<string | undefined>()
const equippedSkin = ref<Skin | null>(null)
const headUrlCache = ref(new Map<string, string>())
const defaultUser = ref()
const equippedSkin = ref(null)
const headUrlCache = ref(new Map())
async function refreshValues() {
defaultUser.value = await get_default_user().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 ?? ''))
accounts.value = await users().catch(handleError)
try {
const skins = await get_available_skins()
equippedSkin.value = skins.find((skin) => skin.is_equipped) ?? null
equippedSkin.value = skins.find((skin) => skin.is_equipped)
if (equippedSkin.value) {
try {
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
headUrlCache.value = new Map(headUrlCache.value).set(
equippedSkin.value.texture_key,
headUrl,
)
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
} catch (error) {
console.warn('Failed to get head render for equipped skin:', error)
}
@@ -163,32 +129,19 @@ async function refreshValues() {
}
}
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) {
function setLoginDisabled(value) {
loginDisabled.value = value
}
defineExpose({
refreshValues,
setEquippedSkin,
setLoginDisabled,
loginDisabled,
})
await refreshValues()
const selectedAccount = computed(() =>
accounts.value.find((account) => account.profile.id === defaultUser.value),
const displayAccounts = computed(() =>
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
)
const avatarUrl = computed(() => {
@@ -205,7 +158,7 @@ const avatarUrl = computed(() => {
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
})
function getAccountAvatarUrl(account: MinecraftCredential) {
function getAccountAvatarUrl(account) {
if (
account.profile.id === selectedAccount.value?.profile?.id &&
equippedSkin.value?.texture_key
@@ -218,10 +171,13 @@ function getAccountAvatarUrl(account: MinecraftCredential) {
return `https://mc-heads.net/avatar/${account.profile.id}/128`
}
async function setAccount(account: MinecraftCredential) {
const selectedAccount = computed(() =>
accounts.value.find((account) => account.profile.id === defaultUser.value),
)
async function setAccount(account) {
defaultUser.value = account.profile.id
await set_default_user(account.profile.id).catch(handleError)
await refreshValues()
emit('change')
}
@@ -231,57 +187,292 @@ async function login() {
if (loggedIn) {
await setAccount(loggedIn)
await refreshValues()
}
trackEvent('AccountLogIn')
loginDisabled.value = false
}
async function logout(id: string) {
const logout = async (id) => {
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>
@@ -1,607 +0,0 @@
<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 writable')) {
if (errorVal.message.includes('directory is not writeable')) {
metadata.value.readOnly = true
}
@@ -1,8 +1,7 @@
<script setup>
import { WrenchIcon, XIcon } from '@modrinth/assets'
import { PlusIcon, XIcon } from '@modrinth/assets'
import {
Accordion,
ButtonStyled,
Button,
Checkbox,
commonMessages,
defineMessages,
@@ -10,7 +9,7 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { save } from '@tauri-apps/plugin-dialog'
import { open } from '@tauri-apps/plugin-dialog'
import { ref } from 'vue'
import { PackageIcon, VersionIcon } from '@/assets/icons'
@@ -41,13 +40,9 @@ const messages = defineMessages({
},
selectFilesLabel: {
id: 'app.export-modal.select-files-label',
defaultMessage: 'Configure which files are included in this export',
defaultMessage: 'Select files and folders to include in pack',
},
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
includeFile: {
id: 'app.export-modal.include-file-accessibility-label',
defaultMessage: 'Include "{file}"?',
},
})
const props = defineProps({
@@ -70,6 +65,7 @@ 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()
@@ -89,9 +85,9 @@ const initFiles = async () => {
disabled:
folder === 'profile.json' ||
folder.startsWith('modrinth_logs') ||
folder.startsWith('.fabric') ||
folder.startsWith('__MACOSX'),
folder.startsWith('.fabric'),
}))
.filter((pathData) => !pathData.path.includes('.DS_Store'))
.forEach((pathData) => {
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
if (parent !== '') {
@@ -125,20 +121,15 @@ const exportPack = async () => {
}
})
})
const outputPath = await save({
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
filters: [
{
name: 'Modrinth Modpack',
extensions: ['mrpack'],
},
],
const outputPath = await open({
directory: true,
multiple: false,
})
if (outputPath) {
export_profile_mrpack(
props.instance.path,
outputPath,
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
filesToExport,
versionInput.value,
exportDescription.value,
@@ -151,104 +142,188 @@ const exportPack = async () => {
<template>
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
<div class="flex flex-col gap-4 w-[40rem]">
<div class="grid grid-cols-2 gap-4">
<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>
<div class="flex flex-col gap-2">
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
<div class="modal-body">
<div class="labeled_input">
<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>
<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"
<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="labeled_input">
<p>{{ formatMessage(commonMessages.descriptionLabel) }}</p>
<StyledInput
v-model="exportDescription"
multiline
:placeholder="formatMessage(messages.descriptionPlaceholder)"
/>
</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)"
>
<template #title>
<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">
<Checkbox
:model-value="children.every((child) => child.selected)"
:indeterminate="
!children.every((child) => child.selected) &&
children.some((child) => child.selected)
"
:description="formatMessage(messages.includeFile, { file: path.name })"
class="pl-4 py-2"
:label="path.name"
class="select-checkbox"
:disabled="children.every((x) => x.disabled)"
@update:model-value="
(newValue) => children.forEach((child) => (child.selected = newValue))
"
@click.stop
/>
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
</template>
<div v-for="child in children" :key="child.path">
<Checkbox
v-model="child.selected"
:label="child.name"
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
:disabled="child.disabled"
v-model="path.showingMore"
class="select-checkbox dropdown"
collapsing-toggle-style
/>
</div>
</Accordion>
<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>
</div>
<Checkbox
v-for="file in files"
:key="file.path"
v-model="file.selected"
:label="file.name"
:disabled="file.disabled"
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
/>
</div>
</Accordion>
<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 class="button-row push-right">
<Button @click="exportModal.hide">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</Button>
<Button color="primary" @click="exportPack">
<PackageIcon />
{{ formatMessage(messages.exportButton) }}
</Button>
</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,44 +1,42 @@
<template>
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
<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 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>
</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 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>
</div>
</div>
</ModalWrapper>
</template>
<script setup>
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
import { Button, injectNotificationManager } from '@modrinth/ui'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
@@ -50,11 +48,6 @@ 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) => {
@@ -80,3 +73,25 @@ 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,104 +1,75 @@
<template>
<JavaDetectionModal ref="detectJavaModal" @submit="(val) => emit('update:modelValue', val)" />
<div :id="props.id" class="toggle-setting" :class="{ compact }">
<div class="input-with-status">
<StyledInput
autocomplete="off"
:disabled="props.disabled"
:model-value="props.modelValue ? props.modelValue.path : ''"
:placeholder="placeholder ?? '/path/to/java'"
wrapper-class="installation-input"
@update:model-value="
(val) => {
emit('update:modelValue', {
...props.modelValue,
path: val,
})
}
"
/>
<ButtonStyled
:color="
!hoveringTest && !testingJava
? testingJavaSuccess === true
? 'green'
: 'red'
: 'standard'
"
color-fill="text"
>
<button
class="!shadow-none"
:disabled="testingJava || props.disabled"
@click="runTest(props.modelValue?.path)"
@mouseenter="!props.disabled && (hoveringTest = true)"
@mouseleave="hoveringTest = false"
>
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
<CheckCircleIcon
v-else-if="testingJavaSuccess === true && !hoveringTest"
class="h-4 w-4"
/>
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
</button>
</ButtonStyled>
</div>
<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">
<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>
<Button
v-if="props.version"
:disabled="props.disabled || installingJava"
@click="reinstallJava"
>
<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>
</span>
</div>
</template>
<script setup>
import {
CheckCircleIcon,
CheckIcon,
DownloadIcon,
FolderSearchIcon,
RefreshCwIcon,
PlayIcon,
SearchIcon,
SpinnerIcon,
XCircleIcon,
XIcon,
} from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
import { Button, injectNotificationManager, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
import { ref } 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 } from '@/helpers/jre.js'
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
const { handleError } = injectNotificationManager()
const props = defineProps({
id: {
type: String,
required: false,
default: null,
},
version: {
type: Number,
required: false,
@@ -129,36 +100,29 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const {
testingJava,
javaTestResult: testingJavaSuccess,
testJavaInstallationDebounced,
testJavaInstallation,
} = useJavaTest()
const testingJava = ref(false)
const testingJavaSuccess = ref(null)
const installingJava = ref(false)
const hoveringTest = ref(false)
let hasInitialized = false
async function runTest(path) {
await testJavaInstallation(path, props.version, true)
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)
}
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()
@@ -168,7 +132,6 @@ async function handleJavaFileInput() {
result = {
path: filePath.path ?? filePath,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -202,7 +165,6 @@ async function reinstallJava() {
result = {
path: path,
version: props.version.toString(),
parsed_version: props.version,
architecture: 'x86',
}
}
@@ -214,23 +176,13 @@ 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 {
flex: 1 1 0;
min-width: 0;
width: 100% !important;
flex-grow: 1;
}
.toggle-setting {
@@ -252,5 +204,17 @@ 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, ButtonStyled } from '@modrinth/ui'
import { Badge, Button } from '@modrinth/ui'
import { computed, ref } from 'vue'
import { SwapIcon } from '@/assets/icons/index.js'
@@ -74,18 +74,15 @@ const onHide = () => {
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
>
<div class="table-cell table-text">
<ButtonStyled
circular
:color="version.id === installedVersion ? 'standard' : 'brand'"
<Button
:color="version.id === installedVersion ? '' : 'primary'"
icon-only
:disabled="inProgress || installing || version.id === installedVersion"
@click.stop="() => switchVersion(version.id)"
>
<button
:disabled="inProgress || installing || version.id === installedVersion"
@click.stop="() => switchVersion(version.id)"
>
<SwapIcon v-if="version.id !== installedVersion" />
<CheckIcon v-else />
</button>
</ButtonStyled>
<SwapIcon v-if="version.id !== installedVersion" />
<CheckIcon v-else />
</Button>
</div>
<div class="name-cell table-cell table-text">
<div class="version-link">
@@ -1,123 +0,0 @@
<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 ease-out;
transition: width 0.3s;
}
</style>
@@ -0,0 +1,476 @@
<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>
@@ -126,7 +126,7 @@ watch(
function fakeLoadingIncrease() {
if (loadingProgress.value < 95) {
setTimeout(() => {
loadingProgress.value += 2
loadingProgress.value += 1
fakeLoadingIncrease()
}, 5)
}
@@ -1,5 +1,5 @@
<script setup>
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
import { Button, injectNotificationManager, ProjectCard } from '@modrinth/ui'
import { ref } from 'vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
@@ -12,6 +12,7 @@ const { install: installVersion } = injectContentInstall()
const confirmModal = ref(null)
const project = ref(null)
const version = ref(null)
const installing = ref(false)
defineExpose({
async show(event) {
@@ -69,9 +70,7 @@ async function install() {
</p>
</div>
<div class="button-group">
<ButtonStyled color="brand">
<button @click="install">Install</button>
</ButtonStyled>
<Button :loading="installing" color="primary" @click="install">Install</Button>
</div>
</div>
</div>
@@ -1,28 +1,18 @@
<template>
<section
v-if="showControls"
class="flex items-center gap-2 mr-1.5"
data-tauri-drag-region-exclude
>
<section v-if="showControls" class="window-controls" data-tauri-drag-region-exclude>
<ButtonStyled type="transparent" circular>
<button class="relative expanded-button" @click="() => getCurrentWindow().minimize()">
<button class="titlebar-button" @click="() => getCurrentWindow().minimize()">
<MinimizeIcon />
</button>
</ButtonStyled>
<ButtonStyled type="transparent" circular>
<button class="relative expanded-button" @click="() => getCurrentWindow().toggleMaximize()">
<button class="titlebar-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">
<ButtonStyled type="transparent" circular>
<button class="titlebar-button close" @click="handleClose">
<XIcon />
</button>
</ButtonStyled>
@@ -34,24 +24,29 @@ 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 { computed, onMounted, onUnmounted, ref, watch } 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 WINDOW_CONTROLS_WIDTH = '8rem'
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')),
const showControls = computed(() => !nativeDecorations.value && os.value !== 'MacOS')
watch(
showControls,
(visible) => {
if (typeof document === 'undefined') return
if (visible) {
document.documentElement.style.setProperty('--window-controls-width', WINDOW_CONTROLS_WIDTH)
} else {
document.documentElement.style.removeProperty('--window-controls-width')
}
},
{ immediate: true },
)
onMounted(async () => {
@@ -72,6 +67,7 @@ onMounted(async () => {
onUnmounted(() => {
unlisten()
document.documentElement.style.removeProperty('--window-controls-width')
})
})
@@ -80,14 +76,23 @@ const handleClose = async () => {
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 lang="scss" scoped>
.window-controls {
position: fixed;
top: 0;
right: 0;
z-index: 10001;
display: flex;
flex-direction: row;
align-items: center;
height: var(--top-bar-height, 3rem);
padding-right: 0.5rem;
gap: 0.25rem;
.titlebar-button.close:hover {
background-color: var(--color-red);
color: var(--color-accent-contrast);
}
}
</style>
@@ -393,6 +393,7 @@ const messages = defineMessages({
<FriendsSection
v-if="pendingFriends.length > 0"
:is-searching="!!search"
open-by-default
:friends="pendingFriends"
:heading="formatMessage(messages.pending)"
:remove-friend="removeFriend"
@@ -0,0 +1,175 @@
<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>
@@ -0,0 +1,418 @@
<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>
@@ -1,140 +0,0 @@
<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 its 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,14 +4,12 @@ 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'
@@ -27,22 +25,14 @@ 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('')
@@ -61,52 +51,6 @@ 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)
@@ -231,38 +175,6 @@ 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',
@@ -392,23 +304,6 @@ 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,12 +8,11 @@ import {
InstallationSettingsLayout,
provideAppBackup,
provideInstallationSettings,
useDebugLogger,
useVIntl,
} from '@modrinth/ui'
import type { GameVersionTag, PlatformTag } from '@modrinth/utils'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref } from 'vue'
import { computed, ref, shallowRef } from 'vue'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version } from '@/helpers/cache'
@@ -35,73 +34,49 @@ import type { Manifest } from '../../../helpers/types'
const { handleError } = injectNotificationManager()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
const debug = useDebugLogger('AppInstallationSettings')
const { instance, offline, isMinecraftServer, onUnlinked, closeModal } = injectInstanceSettings()
const { instance, offline, isMinecraftServer, onUnlinked } = 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,
forge_versions,
quilt_versions,
neoforge_versions,
all_game_versions,
loaders,
] = await Promise.all([
get_loader_versions('fabric')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('forge')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('quilt')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_loader_versions('neo')
.then((manifest: Manifest) => shallowRef(manifest))
.catch(handleError),
get_game_versions()
.then((gameVersions: GameVersionTag[]) => shallowRef(gameVersions))
.catch(handleError),
get_loaders()
.then((value: PlatformTag[]) =>
value
.filter(
(item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla',
)
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
.then((loader: PlatformTag[]) => ref(loader))
.catch(handleError),
])
function getSupportedModpackLoaders() {
return get_loaders().then((value: PlatformTag[]) =>
value
.filter((item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla')
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
}
const fabricVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'fabric'],
queryFn: () => get_loader_versions('fabric') as Promise<Manifest>,
})
const forgeVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'forge'],
queryFn: () => get_loader_versions('forge') as Promise<Manifest>,
})
const quiltVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'quilt'],
queryFn: () => get_loader_versions('quilt') as Promise<Manifest>,
})
const neoforgeVersionsQuery = useQuery({
queryKey: ['instance-settings', 'loader-versions', 'neo'],
queryFn: () => get_loader_versions('neo') as Promise<Manifest>,
})
const gameVersionsQuery = useQuery({
queryKey: ['instance-settings', 'game-versions'],
queryFn: () => get_game_versions() as Promise<GameVersionTag[]>,
})
const loadersQuery = useQuery({
queryKey: ['instance-settings', 'loaders', 'modpack'],
queryFn: getSupportedModpackLoaders,
})
const metadataLoading = computed(() =>
[
fabricVersionsQuery,
forgeVersionsQuery,
quiltVersionsQuery,
neoforgeVersionsQuery,
gameVersionsQuery,
loadersQuery,
].some((query) => query.isLoading.value),
)
debug('metadata queries configured', {
instancePath: instance.value.path,
loader: instance.value.loader,
gameVersion: instance.value.game_version,
})
const modpackInfoQuery = useQuery({
const { data: modpackInfo } = useQuery({
queryKey: computed(() => ['linkedModpackInfo', instance.value.path]),
queryFn: () => get_linked_modpack_info(instance.value.path, 'must_revalidate'),
enabled: computed(() => !!instance.value.linked_data?.project_id && !offline),
})
const modpackInfo = modpackInfoQuery.data
const repairing = ref(false)
const reinstalling = ref(false)
@@ -114,27 +89,17 @@ const messages = defineMessages({
})
function getManifest(loader: string) {
const map: Record<string, Manifest | undefined> = {
fabric: fabricVersionsQuery.data.value,
forge: forgeVersionsQuery.data.value,
quilt: quiltVersionsQuery.data.value,
neoforge: neoforgeVersionsQuery.data.value,
const map: Record<string, typeof fabric_versions> = {
fabric: fabric_versions,
forge: forge_versions,
quilt: quilt_versions,
neoforge: neoforge_versions,
}
const manifest = map[loader]
debug('getManifest:', {
loader,
hasManifest: !!manifest,
gameVersions: manifest?.gameVersions?.length ?? 0,
})
return manifest
return map[loader]
}
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
@@ -144,13 +109,11 @@ 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: computed(() => metadataLoading.value || modpackInfoQuery.isLoading.value),
loading: ref(false),
installationInfo: computed(() => {
const rows = [
{
@@ -192,80 +155,41 @@ provideInstallationSettings({
currentPlatform: computed(() => instance.value.loader),
currentGameVersion: computed(() => instance.value.game_version),
currentLoaderVersion: computed(() => instance.value.loader_version ?? ''),
availablePlatforms: computed(() => loadersQuery.data.value?.map((x) => x.name) ?? []),
availablePlatforms: loaders?.value?.map((x) => x.name) ?? [],
resolveGameVersions(loader, showSnapshots) {
const versions = gameVersionsQuery.data.value ?? []
const versions = all_game_versions?.value ?? []
const filtered = versions.filter((item) => {
if (loader === 'vanilla') return true
const manifest = getManifest(loader)
return !!manifest?.gameVersions?.some((x) => item.version === x.id)
return !!manifest?.value?.gameVersions?.some((x) => item.version === x.id)
})
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
return (showSnapshots ? filtered : filtered.filter((x) => x.version_type === 'release')).map(
(x) => ({ value: x.version, label: x.version }),
)
},
resolveLoaderVersions(loader, gameVersion) {
if (loader === 'vanilla' || !gameVersion) {
debug('resolveLoaderVersions: skipped', { loader, gameVersion })
return []
}
if (loader === 'vanilla' || !gameVersion) return []
const manifest = getManifest(loader)
if (!manifest) {
debug('resolveLoaderVersions: no manifest', { loader, gameVersion })
return []
}
if (!manifest?.value) return []
if (loader === 'fabric' || loader === 'quilt') {
const result = manifest.gameVersions[0]?.loaders ?? []
debug('resolveLoaderVersions: fabric/quilt result', {
loader,
gameVersion,
count: result.length,
})
return result
return manifest.value.gameVersions[0]?.loaders ?? []
}
const result = manifest.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
debug('resolveLoaderVersions: result', { loader, gameVersion, count: result.length })
return result
return manifest.value.gameVersions?.find((item) => item.id === gameVersion)?.loaders ?? []
},
resolveHasSnapshots(loader) {
const versions = gameVersionsQuery.data.value ?? []
if (loader === 'vanilla') {
const result = versions.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots: vanilla', { loader, result })
return result
}
const versions = all_game_versions?.value ?? []
if (loader === 'vanilla') return versions.some((x) => x.version_type !== 'release')
const manifest = getManifest(loader)
const supported = versions.filter(
(item) => !!manifest?.gameVersions?.some((x) => item.version === x.id),
(item) => !!manifest?.value?.gameVersions?.some((x) => item.version === x.id),
)
const result = supported.some((x) => x.version_type !== 'release')
debug('resolveHasSnapshots:', {
loader,
totalVersions: versions.length,
supportedVersions: supported.length,
result,
})
return result
return supported.some((x) => x.version_type !== 'release')
},
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,
@@ -274,21 +198,17 @@ 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
@@ -296,11 +216,9 @@ 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
@@ -308,11 +226,9 @@ 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,
})
@@ -320,38 +236,27 @@ 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,15 +1,6 @@
<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,
@@ -17,11 +8,9 @@ import {
StyledInput,
useVIntl,
} from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { computed, readonly, ref, watch } from 'vue'
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
import useJavaTest from '@/composables/useJavaTest'
import JavaSelector from '@/components/ui/JavaSelector.vue'
import useMemorySlider from '@/composables/useMemorySlider'
import { edit, get_optimal_jre_key } from '@/helpers/profile'
import { get } from '@/helpers/settings.ts'
@@ -36,54 +25,9 @@ const { instance } = injectInstanceSettings()
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
const optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const 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 optimalJava = readonly(await get_optimal_jre_key(instance.value.path).catch(handleError))
const javaInstall = ref({ path: optimalJava.path ?? instance.value.java_path })
const overrideJavaArgs = ref((instance.value.extra_launch_args?.length ?? 0) > 0)
const javaArgs = ref(
@@ -107,8 +51,8 @@ const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) a
const editProfileObject = computed(() => {
return {
java_path:
overrideJavaInstall.value && javaPath.value
? javaPath.value.replace('java.exe', 'javaw.exe')
overrideJavaInstall.value && javaInstall.value.path !== ''
? javaInstall.value.path.replace('java.exe', 'javaw.exe')
: null,
extra_launch_args: overrideJavaArgs.value
? javaArgs.value.trim().split(/\s+/).filter(Boolean)
@@ -127,7 +71,7 @@ const editProfileObject = computed(() => {
watch(
[
overrideJavaInstall,
javaPath,
javaInstall,
overrideJavaArgs,
javaArgs,
overrideEnvVars,
@@ -146,45 +90,17 @@ 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',
},
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...',
javaMemory: {
id: 'instance.settings.tabs.java.java-memory',
defaultMessage: 'Memory allocated',
},
hooks: {
id: 'instance.settings.tabs.java.hooks',
@@ -195,86 +111,43 @@ const messages = defineMessages({
<template>
<div>
<JavaDetectionModal ref="javaDetectionModal" @submit="(val) => (javaPath = val.path)" />
<h2 class="m-0 mb-2 text-lg font-extrabold text-contrast block">
<h2 id="project-name" class="m-0 mb-2.5 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.javaInstallation) }}
</h2>
<Checkbox
v-model="overrideJavaInstall"
:label="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
<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
>
<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>
</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>
</div>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
<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">
{{ formatMessage(messages.javaMemory) }}
</h2>
<Checkbox
v-model="overrideMemorySettings"
:label="formatMessage(messages.customMemoryAllocation)"
class="mb-2"
/>
<Checkbox v-model="overrideMemorySettings" label="Custom memory allocation" class="mb-2.5" />
<Slider
id="max-memory"
v-model="memory.maximum"
@@ -286,36 +159,28 @@ const messages = defineMessages({
:snap-range="512"
unit="MB"
/>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.javaArguments) }}
</h2>
<Checkbox
v-model="overrideJavaArgs"
:label="formatMessage(messages.customJavaArguments)"
class="my-2"
/>
<Checkbox v-model="overrideJavaArgs" label="Custom java arguments" class="my-2" />
<StyledInput
id="java-args"
v-model="javaArgs"
autocomplete="off"
:disabled="!overrideJavaArgs"
:placeholder="formatMessage(messages.enterJavaArguments)"
placeholder="Enter java arguments..."
wrapper-class="w-full"
/>
<h2 class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
<h2 id="project-name" class="mt-6 mb-2.5 text-lg font-semibold text-contrast block">
{{ formatMessage(messages.javaEnvironmentVariables) }}
</h2>
<Checkbox
v-model="overrideEnvVars"
:label="formatMessage(messages.customEnvironmentVariables)"
class="mb-2"
/>
<Checkbox v-model="overrideEnvVars" label="Custom environment variables" class="mb-2.5" />
<StyledInput
id="env-vars"
v-model="envVars"
autocomplete="off"
:disabled="!overrideEnvVars"
:placeholder="formatMessage(messages.enterEnvironmentVariables)"
placeholder="Enter environmental variables..."
wrapper-class="w-full"
/>
</div>
@@ -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 { findMinecraftAuthError, type MinecraftAuthError } from './minecraft-auth-errors'
import { type MinecraftAuthError, minecraftAuthErrors } 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 = findMinecraftAuthError(rawError.value)
matchedError.value = minecraftAuthErrors.find((e) => rawError.value.includes(e.errorCode)) ?? null
debugCollapsed.value = true
hide_ads_window()
@@ -1,93 +1,10 @@
export interface MinecraftAuthError {
errorCode?: string
errorMatchers?: string[]
matches?: (message: string) => boolean
errorCode: string
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:
@@ -170,31 +87,4 @@ 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 @click="modal?.hide()">
<button class="!border !border-surface-4" @click="modal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
@@ -16,8 +16,7 @@ import {
type TabbedModalTab,
useVIntl,
} from '@modrinth/ui'
import type { PlatformTag } from '@modrinth/utils'
import { useQuery } from '@tanstack/vue-query'
import { useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, nextTick, ref, watch } from 'vue'
@@ -27,9 +26,7 @@ import InstallationSettings from '@/components/ui/instance_settings/Installation
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
import { get_project_v3 } from '@/helpers/cache'
import { get_loader_versions } from '@/helpers/metadata'
import { get_linked_modpack_info } from '@/helpers/profile'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { provideInstanceSettings } from '@/providers/instance-settings'
import type { GameInstance } from '../../../helpers/types'
@@ -48,18 +45,12 @@ const isMinecraftServer = ref(false)
const handleUnlinked = () => emit('unlinked')
const instanceRef = computed(() => props.instance)
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(
@@ -122,53 +113,23 @@ const tabs = computed<TabbedModalTab[]>(() => [
},
])
function getSupportedModpackLoaders() {
return get_loaders().then((value: PlatformTag[]) =>
value
.filter((item) => item.supported_project_types.includes('modpack') || item.name === 'vanilla')
.sort((a, b) => (a.name === 'vanilla' ? -1 : b.name === 'vanilla' ? 1 : 0)),
)
}
// Preload
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'fabric'],
queryFn: () => get_loader_versions('fabric'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'forge'],
queryFn: () => get_loader_versions('forge'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'quilt'],
queryFn: () => get_loader_versions('quilt'),
})
useQuery({
queryKey: ['instance-settings', 'loader-versions', 'neo'],
queryFn: () => get_loader_versions('neo'),
})
useQuery({
queryKey: ['instance-settings', 'game-versions'],
queryFn: get_game_versions,
})
useQuery({
queryKey: ['instance-settings', 'loaders', 'modpack'],
queryFn: getSupportedModpackLoaders,
})
useQuery({
queryKey: computed(() => ['linkedModpackInfo', props.instance.path]),
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
enabled: computed(() => !!props.instance.linked_data?.project_id && !props.offline),
})
const queryClient = useQueryClient()
const tabbedModal = ref<InstanceType<typeof TabbedModal> | null>(null)
function show(tabIndex?: number) {
if (props.instance.linked_data?.project_id) {
queryClient.prefetchQuery({
queryKey: ['linkedModpackInfo', props.instance.path],
queryFn: () => get_linked_modpack_info(props.instance.path, 'stale_while_revalidate'),
})
}
tabbedModal.value?.show()
if (tabIndex !== undefined) {
nextTick(() => tabbedModal.value?.setTab(tabIndex))
}
}
defineExpose({ show, hide })
defineExpose({ show })
</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 @click="handleCancel">
<button class="!border !border-surface-4" @click="handleCancel">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
@@ -1,115 +1,13 @@
<script setup lang="ts">
import { Combobox, defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
import { Combobox, ThemeSelector, Toggle } 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, FeatureFlag } from '@/store/theme.ts'
import type { ColorTheme } 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())
@@ -123,10 +21,8 @@ watch(
)
</script>
<template>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.colorThemeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
<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>
<ThemeSelector
:update-color-theme="
@@ -142,11 +38,10 @@ watch(
<div class="mt-6 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-semibold text-contrast">
{{ formatMessage(messages.advancedRenderingTitle) }}
</h2>
<h2 class="m-0 text-lg font-semibold text-contrast">Advanced rendering</h2>
<p class="m-0 mt-1">
{{ formatMessage(messages.advancedRenderingDescription) }}
Enables advanced rendering such as blur effects that may cause performance issues without
hardware-accelerated rendering.
</p>
</div>
@@ -162,124 +57,57 @@ 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">
{{ formatMessage(messages.nativeDecorationsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
<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>
</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">
{{ formatMessage(messages.minimizeLauncherTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.minimizeLauncherDescription) }}</p>
<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>
</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">
{{ formatMessage(messages.showPlayTimeTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
@update:model-value="
() => {
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
themeStore.featureFlags[showPlayTimeFlag] = newValue
settings.feature_flags[showPlayTimeFlag] = newValue
}
"
/>
</div>
<div class="mt-6 flex items-center justify-between">
<div>
<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>
<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="[
{
value: 'Home',
label: formatMessage(messages.defaultLandingPageHome),
},
{
value: 'Library',
label: formatMessage(messages.defaultLandingPageLibrary),
},
]"
: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">
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
</h2>
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
<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>
</div>
<Toggle
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
@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
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
themeStore.featureFlags['worlds_in_home'] = newValue
settings.feature_flags['worlds_in_home'] = newValue
}
"
/>
@@ -287,10 +115,8 @@ watch(
<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>
<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>
</div>
<Toggle
id="toggle-sidebar"
@@ -1,6 +1,6 @@
<script setup>
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
import { Button, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
import { open } from '@tauri-apps/plugin-dialog'
import { ref, watch } from 'vue'
@@ -73,11 +73,9 @@ async function findLauncherDir() {
wrapper-class="w-full"
>
<template #right>
<ButtonStyled circular>
<button class="ml-1.5" @click="findLauncherDir">
<FolderSearchIcon />
</button>
</ButtonStyled>
<Button class="ml-1.5" @click="findLauncherDir">
<FolderSearchIcon />
</Button>
</template>
</StyledInput>
<p class="m-0 leading-tight text-secondary">
@@ -1,241 +1,146 @@
<template>
<NewModal ref="modal" :on-hide="handleModalHide">
<UploadSkinModal ref="uploadModal" />
<ModalWrapper ref="modal" @on-hide="resetState">
<template #title>
<span class="text-lg font-extrabold text-contrast">
{{ formatMessage(mode === 'edit' ? messages.editSkinTitle : messages.addSkinTitle) }}
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
</span>
</template>
<div class="flex flex-col md:flex-row gap-6">
<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 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>
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
<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>
<h2 class="text-base font-semibold mb-2">Texture</h2>
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
</section>
<section v-if="canEditTextureAndModel">
<h2 class="text-base font-semibold mb-2">
{{ formatMessage(messages.armStyleSection) }}
</h2>
<section>
<h2 class="text-base font-semibold mb-2">Arm style</h2>
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
<template #default="{ item }">
{{
formatMessage(item === 'CLASSIC' ? messages.wideArmStyle : messages.slimArmStyle)
}}
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
</template>
</RadioButtons>
</section>
<section>
<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"
<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)"
>
<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>
<span>Use default cape</span>
</CapeButton>
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
<span>Use default cape</span>
</CapeLikeTextButton>
<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"
<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"
>
<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>
<template #icon><ChevronRightIcon /></template>
<span>More</span>
</CapeLikeTextButton>
</div>
</section>
</div>
</div>
<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>
<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>
<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 { arrayBufferToBase64 } from '@modrinth/utils'
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
import { computed, 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,
normalize_skin_texture,
save_custom_skin,
remove_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 textureFileInput = useTemplateRef<HTMLInputElement>('textureFileInput')
const capeListRef = ref<HTMLElement | null>(null)
const capeListMaxHeight = ref(`${CAPE_LIST_MAX_HEIGHT}px`)
const selectCapeModal = useTemplateRef('selectCapeModal')
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)
@@ -243,49 +148,10 @@ const previewSkin = ref<string>('')
const variant = ref<SkinModel>('CLASSIC')
const selectedCape = ref<Cape | undefined>(undefined)
const props = defineProps<{ capes?: Cape[] }>()
const props = defineProps<{ capes?: Cape[]; defaultCape?: Cape }>()
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
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 visibleCapeList = ref<Cape[]>([])
const sortedCapes = computed(() => {
return [...(props.capes || [])].sort((a, b) => {
@@ -295,6 +161,32 @@ 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
@@ -326,13 +218,9 @@ const disableSave = computed(
)
const saveTooltip = computed(() => {
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)
}
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!'
return undefined
})
@@ -343,13 +231,11 @@ 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
@@ -360,11 +246,12 @@ 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) {
@@ -373,54 +260,98 @@ 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 setUploadedTexture(skinTextureUrl: SkinTextureUrl) {
async function restoreWithNewTexture(skinTextureUrl: SkinTextureUrl) {
uploadedTextureUrl.value = skinTextureUrl
await loadPreviewSkin()
nextTick(() => refreshCapeListLayout())
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
shouldRestoreModal.value = false
}, 0)
}
}
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 openTextureFileBrowser() {
textureFileInput.value?.click()
function handleCapeSelected(cape: Cape | undefined) {
selectCape(cape)
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)
}
}
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 = ''
}
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)
}
}
@@ -436,45 +367,17 @@ async function save() {
textureUrl = currentSkin.value!.texture
}
await unequip_skin()
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
if (mode.value === 'new') {
const addedSkin = await save_custom_skin(
{
texture_key: '',
variant: variant.value,
cape_id: selectedCape.value?.id,
texture: textureUrl,
source: 'custom',
is_equipped: false,
},
bytes,
variant.value,
selectedCape.value,
true,
)
emit('saved', {
applied: false,
skin: addedSkin,
})
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
emit('saved')
} else {
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!,
})
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
await remove_custom_skin(currentSkin.value!)
emit('saved')
}
hide()
@@ -487,53 +390,28 @@ async function save() {
watch([uploadedTextureUrl, currentSkin], async () => {
await loadPreviewSkin()
refreshCapeListLayout()
})
watch(
() => props.capes,
() => {
nextTick(() => refreshCapeListLayout())
initVisibleCapeList()
},
{ 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', options: { applied: boolean; skin?: Skin; previousSkin?: Skin }): void
(event: 'saved'): void
(event: 'deleted', skin: Skin): void
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
}>()
defineExpose({
show,
showNew,
restoreWithNewTexture,
hide,
shouldRestoreModal,
restoreModal,
})
</script>
@@ -0,0 +1,141 @@
<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>
@@ -0,0 +1,141 @@
<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>
@@ -1,578 +0,0 @@
<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 Draggable from 'vuedraggable'
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
readOnly?: boolean
}>()
const emit = defineEmits<{
select: [skin: Skin]
edit: [skin: Skin, event: MouseEvent]
delete: [skin: Skin]
'reorder-saved-skins': [skins: 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 draggableSavedSkins = ref<Skin[]>([])
const isDraggingSavedSkin = ref(false)
const canReorderSavedSkins = computed(() => draggableSavedSkins.value.length > 1)
const fixedSavedSkins = computed(() => props.savedSkins.filter((skin) => !canDragSavedSkin(skin)))
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(
() => props.savedSkins,
(nextSkins) => {
if (isDraggingSavedSkin.value) {
return
}
draggableSavedSkins.value = nextSkins.filter(canDragSavedSkin)
},
{ 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 savedSkinKey(skin: Skin) {
return skinKey(skin, 'saved-skin')
}
function canDragSavedSkin(skin: Skin) {
return skin.source === 'custom' || skin.source === 'custom_external'
}
function doSkinOrdersMatch(firstSkins: Skin[], secondSkins: Skin[]) {
const draggableSecondSkins = secondSkins.filter(canDragSavedSkin)
return (
firstSkins.length === draggableSecondSkins.length &&
firstSkins.every(
(skin, index) => savedSkinKey(skin) === savedSkinKey(draggableSecondSkins[index]),
)
)
}
function onSavedSkinDragStart() {
isDraggingSavedSkin.value = true
}
function onSavedSkinDragEnd() {
isDraggingSavedSkin.value = false
if (doSkinOrdersMatch(draggableSavedSkins.value, props.savedSkins)) {
draggableSavedSkins.value = props.savedSkins.filter(canDragSavedSkin)
return
}
emit('reorder-saved-skins', [...draggableSavedSkins.value])
}
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>
<Draggable
v-if="section.kind === 'saved'"
:list="draggableSavedSkins"
class="grid w-full grid-cols-3 gap-3 min-[1300px]:grid-cols-4 min-[1750px]:grid-cols-5 min-[2050px]:grid-cols-6"
:item-key="savedSkinKey"
:disabled="readOnly || !canReorderSavedSkins"
:animation="250"
:swap-threshold="1"
:invert-swap="false"
:force-fallback="true"
:fallback-on-body="true"
:fallback-tolerance="4"
ghost-class="skin-reorder-ghost"
chosen-class="skin-reorder-chosen"
drag-class="skin-reorder-drag"
fallback-class="skin-reorder-fallback"
@start="onSavedSkinDragStart"
@end="onSavedSkinDragEnd"
>
<template #header>
<SkinLikeTextButton
ref="addSkinButton"
class="aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
dropzone
:disabled="readOnly"
:drag-active="!readOnly && 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>
</template>
<template #item="{ element: skin }">
<div
:key="savedSkinKey(skin)"
class="relative aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
>
<SkinButton
class="h-full w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:disabled="readOnly"
:is-dragging="isDraggingSavedSkin"
@select="emit('select', skin)"
>
<template v-if="!readOnly" #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>
</template>
<template #footer>
<div
v-for="skin in fixedSavedSkins"
:key="savedSkinKey(skin)"
class="relative aspect-[31/40] w-full min-w-0 box-border rounded-[20px]"
>
<SkinButton
class="h-full w-full min-w-0 box-border rounded-[20px]"
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:disabled="readOnly"
:is-dragging="isDraggingSavedSkin"
@select="emit('select', skin)"
>
<template v-if="!readOnly" #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>
</template>
</Draggable>
<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"
:selected="isSkinSelected(skin)"
:active="isSkinActive(skin)"
:tooltip="skin.name"
:disabled="readOnly"
:is-dragging="isDraggingSavedSkin"
@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>
</template>
</SkinButton>
</div>
</Accordion>
</div>
</div>
</template>
<style scoped>
:global(.skin-reorder-ghost) {
opacity: 0.35;
}
:global(.skin-reorder-drag) {
cursor: grabbing;
}
:global(.skin-reorder-fallback) {
opacity: 0.9;
pointer-events: none;
}
</style>
@@ -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 flex-nowrap whitespace-nowrap"
class="text-sm text-secondary flex items-center gap-1 font-semibold"
>
<UserIcon
aria-hidden="true"
@@ -93,7 +93,7 @@ defineExpose({ show, hide })
<template #actions>
<div class="flex gap-2 justify-end">
<ButtonStyled type="outlined">
<button @click="hide()">
<button class="!border !border-surface-4" @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 @click="hide()">
<button class="!border !border-surface-4" @click="hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
@@ -1,332 +0,0 @@
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
output?: string | null
stdout?: string
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, true)
const logs: LogEntry[] = await get_logs(instancePath, false)
entry.logList = logs
for (const log of logs) {
if (log.output) {
entry.historicalCache.set(log.filename, log.output)
if (log.stdout && log.stdout !== '') {
entry.historicalCache.set(log.filename, log.stdout)
}
}
@@ -1,52 +0,0 @@
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,
}
}
-4
View File
@@ -98,10 +98,6 @@ 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 {
@@ -1,100 +0,0 @@
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
})
}
+3 -2
View File
@@ -6,11 +6,12 @@
import { invoke } from '@tauri-apps/api/core'
/*
A log is a struct containing the filename string and optional output, as follows:
A log is a struct containing the filename string, stdout, and stderr, as follows:
pub struct Logs {
pub filename: String,
pub output: Option<String>,
pub stdout: String,
pub stderr: String,
}
*/
+10 -25
View File
@@ -8,7 +8,6 @@ interface PackProfileCreator {
gameVersion: string
modloader: InstanceLoader
loaderVersion: string | null
unknownFile: boolean
}
interface PackLocationVersionId {
@@ -70,10 +69,7 @@ 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,
showUnknownPackWarningModal?: (createProfile: () => Promise<void>, fileName: string) => void,
): Promise<void> {
export async function create_profile_and_install_from_file(path: string): Promise<void> {
const location: PackLocationFile = {
type: 'fromFile',
path,
@@ -82,24 +78,13 @@ export async function create_profile_and_install_from_file(
'plugin:pack|pack_get_profile_from_pack',
{ location },
)
const createProfile = async () => {
const profile = await create(
profile_creator.name,
profile_creator.gameVersion,
profile_creator.modloader,
profile_creator.loaderVersion,
null,
true,
)
await invoke('plugin:pack|pack_install', { location, profile })
}
if (profile_creator.unknownFile && showUnknownPackWarningModal) {
const splitPath = path.split(/[\\/]/)
const fileName = splitPath ? splitPath[splitPath.length - 1] : path
showUnknownPackWarningModal(createProfile, fileName)
} else {
await createProfile()
}
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 })
}
+3 -22
View File
@@ -147,16 +147,9 @@ 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<JavaVersion | null> {
export async function get_optimal_jre_key(path: string): Promise<string | null> {
return await invoke('plugin:profile|profile_get_optimal_jre_key', { path })
}
@@ -191,20 +184,8 @@ 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 type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
export async function add_project_from_version(
path: string,
versionId: string,
reason: DownloadReason,
dependentOnVersionId?: string,
): Promise<string> {
return await invoke('plugin:profile|profile_add_project_from_version', {
path,
versionId,
reason,
dependentOnVersionId,
})
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 })
}
// Add a project to a profile from a path + project_type
@@ -1,83 +0,0 @@
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,8 +3,9 @@ import {
applyCapeTexture,
createTransparentTexture,
disposeCaches,
loadTexture,
setupSkinModel,
} from '@modrinth/ui'
} from '@modrinth/utils'
import * as THREE from 'three'
import { reactive } from 'vue'
@@ -15,10 +16,12 @@ import { skinPreviewStorage } from '../storage/skin-preview-storage'
export interface RenderResult {
forwards: string
backwards: string
}
export interface RawRenderResult {
forwards: Blob
backwards: Blob
}
class BatchSkinRenderer {
@@ -26,7 +29,6 @@ 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
@@ -50,7 +52,6 @@ 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)
@@ -61,7 +62,7 @@ class BatchSkinRenderer {
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
directionalLight.castShadow = false
directionalLight.castShadow = true
directionalLight.position.set(2, 4, 3)
this.scene.add(ambientLight)
this.scene.add(directionalLight)
@@ -90,9 +91,12 @@ class BatchSkinRenderer {
}
const frontCameraPos: [number, number, number] = [-1.3, 1, 6.3]
const forwards = await this.renderView(frontCameraPos, lookAtTarget)
const backCameraPos: [number, number, number] = [-1.3, 1, -2.5]
return { forwards }
const forwards = await this.renderView(frontCameraPos, lookAtTarget)
const backwards = await this.renderView(backCameraPos, lookAtTarget)
return { forwards, backwards }
}
private async renderView(
@@ -108,19 +112,9 @@ class BatchSkinRenderer {
this.renderer.render(this.scene, this.camera)
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,
)
})
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
const response = await fetch(dataUrl)
return await response.blob()
}
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
@@ -128,10 +122,14 @@ class BatchSkinRenderer {
throw new Error('Renderer not initialized')
}
const { model } = await setupSkinModel(modelUrl, textureUrl, capeUrl)
const { model } = await setupSkinModel(modelUrl, textureUrl)
if (!capeUrl) {
applyCapeTexture(model, null, this.getTransparentTexture())
if (capeUrl) {
const capeTexture = await loadTexture(capeUrl)
applyCapeTexture(model, capeTexture)
} else {
const transparentTexture = createTransparentTexture()
applyCapeTexture(model, null, transparentTexture)
}
const group = new THREE.Group()
@@ -143,38 +141,39 @@ class BatchSkinRenderer {
this.currentModel = group
}
private getTransparentTexture(): THREE.Texture {
if (!this.transparentTexture) {
this.transparentTexture = createTransparentTexture()
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()
}
}
}
}
return this.transparentTexture
}
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)
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()
}
}
@@ -195,9 +194,6 @@ 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()
@@ -360,27 +356,7 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
return await generateHeadRender(skin)
}
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
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
try {
const skinKeys = skins.map(
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
@@ -392,35 +368,33 @@ async function generateSkinPreviewsForGeneration(
headStorage.batchRetrieve(headKeys),
])
if (!isCurrentGeneration()) return
for (let i = 0; i < skins.length; i++) {
const skinKey = skinKeys[i]
const headKey = headKeys[i]
const rawCached = cachedSkinPreviews[skinKey]
if (rawCached && !skinBlobUrlMap.has(skinKey)) {
if (rawCached) {
const cached: RenderResult = {
forwards: URL.createObjectURL(rawCached.forwards),
backwards: URL.createObjectURL(rawCached.backwards),
}
skinBlobUrlMap.set(skinKey, cached)
}
const cachedHead = cachedHeadPreviews[headKey]
if (cachedHead && !headBlobUrlMap.has(headKey)) {
if (cachedHead) {
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
}
}
for (const skin of skins) {
if (!isCurrentGeneration()) return
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
if (skinBlobUrlMap.has(key)) {
if (DEBUG_MODE) {
const result = skinBlobUrlMap.get(key)!
URL.revokeObjectURL(result.forwards)
URL.revokeObjectURL(result.backwards)
skinBlobUrlMap.delete(key)
} else continue
}
@@ -445,10 +419,9 @@ async function generateSkinPreviewsForGeneration(
cape?.texture,
)
if (!isCurrentGeneration()) return
const renderResult: RenderResult = {
forwards: URL.createObjectURL(rawRenderResult.forwards),
backwards: URL.createObjectURL(rawRenderResult.backwards),
}
skinBlobUrlMap.set(key, renderResult)
@@ -466,12 +439,9 @@ async function generateSkinPreviewsForGeneration(
}
} finally {
disposeSharedRenderer()
await cleanupUnusedPreviews(skins)
if (isCurrentGeneration()) {
await cleanupUnusedPreviews(skins)
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
}
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
}
}
+10 -36
View File
@@ -5,6 +5,7 @@ export interface Cape {
id: string
name: string
texture: string
is_default: boolean
is_equipped: boolean
}
@@ -14,7 +15,6 @@ 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,11 +121,17 @@ export async function get_available_skins(): Promise<Skin[]> {
export async function add_and_equip_custom_skin(
textureBlob: Uint8Array,
variant: SkinModel,
cape?: Cape,
): Promise<Skin> {
return await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
capeOverride?: Cape,
): Promise<void> {
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,
})
}
@@ -142,28 +148,6 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
})
}
export async function set_custom_skin_order(textureKeys: string[]): Promise<void> {
await invoke('plugin:minecraft-skins|set_custom_skin_order', {
textureKeys,
})
}
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)
@@ -178,16 +162,6 @@ 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,47 +5,15 @@
*/
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<void>('initialize_state')
return await invoke('initialize_state')
}
// Gets active progress bars
export async function progress_bars_list() {
return await invoke<Record<string, LoadingBar>>('plugin:utils|progress_bars_list')
return await invoke('plugin:utils|progress_bars_list')
}
// Get opening command
@@ -53,5 +21,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<OpeningCommand | null>('plugin:utils|get_opening_command')
return await invoke('plugin:utils|get_opening_command')
}
@@ -2,6 +2,7 @@ import type { RawRenderResult } from '../rendering/batch-skin-renderer'
interface StoredPreview {
forwards: Blob
backwards: Blob
timestamp: number
}
@@ -37,6 +38,7 @@ export class SkinPreviewStorage {
const storedPreview: StoredPreview = {
forwards: result.forwards,
backwards: result.backwards,
timestamp: Date.now(),
}
@@ -65,7 +67,7 @@ export class SkinPreviewStorage {
return
}
resolve({ forwards: result.forwards })
resolve({ forwards: result.forwards, backwards: result.backwards })
}
request.onerror = () => reject(request.error)
})
@@ -93,7 +95,7 @@ export class SkinPreviewStorage {
const result = request.result as StoredPreview | undefined
if (result) {
results[key] = { forwards: result.forwards }
results[key] = { forwards: result.forwards, backwards: result.backwards }
} else {
results[key] = null
}
@@ -171,7 +173,7 @@ export class SkinPreviewStorage {
const key = cursor.primaryKey as string
const value = cursor.value as StoredPreview
const entrySize = value.forwards.size
const entrySize = value.forwards.size + value.backwards.size
totalSize += entrySize
count++
-3
View File
@@ -14,7 +14,6 @@ export type GameInstance = {
groups: string[]
linked_data?: LinkedData
preferred_update_channel: ReleaseChannel
created: Date
modified: Date
@@ -47,8 +46,6 @@ type LinkedData = {
locked: boolean
}
type ReleaseChannel = 'release' | 'beta' | 'alpha'
export type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
type ContentFile = {
@@ -1,23 +0,0 @@
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
}
-4
View File
@@ -22,10 +22,6 @@ 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')
+30 -186
View File
@@ -1,85 +1,16 @@
{
"app.action-bar.downloading-java": {
"message": "جاز تنزيل إصدار جافا {version}"
},
"app.action-bar.downloading-update": {
"message": "تحميل التحديث"
},
"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.reload-to-update": {
"message": "أعد التشغيل للتحديث"
},
"app.action-bar.show-more-running-instances": {
"message": "عرض المزيد من النسخ قيد التشغيل"
},
"app.action-bar.stop-instance": {
"message": "إيقاف النسخة"
},
"app.action-bar.update": {
"message": "تحديث"
},
"app.action-bar.view-active-downloads": {
"message": "عرض التحميلات الفعالة"
},
"app.action-bar.view-instance": {
"message": "عرض النسخة"
},
"app.action-bar.view-logs": {
"message": "عرض التسجيل"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "يتيح العرض المتقدم لتأثيرات مثل التشويش الذي قد تسبب مشكلات في الأداء دون استخدام العرض المسرع بالأجهزة."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "العرض المتقدم"
},
"app.appearance-settings.color-theme.description": {
"message": "اختر لون سمتك المفضل من أجل تطبيق Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "لون السمة"
},
"app.appearance-settings.default-landing-page.description": {
"message": "غير الصفحة التي يفتح عليها المشغل."
},
"app.appearance-settings.default-landing-page.home": {
"message": "صفحة الرئيسية"
},
"app.appearance-settings.default-landing-page.library": {
"message": "مكتبة"
},
"app.appearance-settings.default-landing-page.title": {
"message": "الصفحة الرئيسة الافتراضية"
},
"app.appearance-settings.hide-nametag.description": {
"message": "ألغ تفعيل عرض الاسم فوق لاعبك في صفحة المظاهر."
},
"app.appearance-settings.hide-nametag.title": {
"message": "إخفاء عرض الاسم"
},
"app.auth-servers.unreachable.body": {
"message": "قد تكون خوادم مصادقة ماينكرافت معطلة حاليًا. تحقق من اتصالك بالإنترنت وحاول مرة أخرى لاحقًا."
},
"app.auth-servers.unreachable.header": {
"message": "تعذر الوصول إلى خوادم المصادقة"
},
"app.browse.add-server-to-instance": {
"message": "أضف الخادم للنموذج"
},
"app.browse.add-servers-to-instance": {
"message": "أضف الخوادم لنموذجك"
},
"app.browse.add-to-instance": {
"message": "أضف للنموذج"
},
@@ -92,9 +23,6 @@
"app.browse.already-added": {
"message": "مضاف فعلا"
},
"app.browse.back-to-instance": {
"message": "العودة للنموذج"
},
"app.browse.discover-content": {
"message": "استكشف محتوى"
},
@@ -102,16 +30,13 @@
"message": "استكشف خوادم"
},
"app.browse.hide-added-servers": {
"message": "إخفاء الخوادم المضافة مسبقًا"
"message": "إخفاء الخوادم المضافة"
},
"app.browse.server.installing": {
"message": "جاري التثبيت"
"app.browse.hide-installed-content": {
"message": "إخفاء المحتوى المضاف"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "تنزيل الحزم"
"app.browse.install-content-to-instance": {
"message": "تثبيت محتوى لنموذجك"
},
"app.export-modal.description-placeholder": {
"message": "أدخل وصف التعديل..."
@@ -129,7 +54,7 @@
"message": "إسم حزمة التعديل"
},
"app.export-modal.select-files-label": {
"message": "أختيار الملفات التي سيتم تصديرها"
"message": "حدد الملفات والمجلدات المراد تضمينها في الحزمة"
},
"app.export-modal.version-number-label": {
"message": "رقم الإصدار"
@@ -150,13 +75,13 @@
"message": "حذف النموذج"
},
"app.instance.modpack-already-installed.body": {
"message": "حُزْمَة التعديل مثبته مسبقًا فعلًا في نموذج <bold>{instanceName}</bold>. هل أنت متأكد بإرادة نسخه؟"
"message": "حُزْمَة التعديل هذه مثبته فعلًا في نموذج <bold>{instanceName}</bold>. هل انت متأكد بإرادة نسخه؟"
},
"app.instance.modpack-already-installed.create": {
"message": "إنشاء"
},
"app.instance.modpack-already-installed.header": {
"message": "حُزْمَة التعديل مثبتة فعلًا"
"message": "حُزْمَة التعديل مثبتة بالفعل"
},
"app.instance.modpack-already-installed.instance": {
"message": "النموذج"
@@ -174,7 +99,7 @@
"message": "تحقق من المشاريع التي أستخدمها في حزمة التعديل الخاص بي!"
},
"app.instance.mods.share-title": {
"message": "مشاركة محتوى حُزْمَة التعديل"
"message": "مشاركة محتوى حزمة التعديل"
},
"app.instance.mods.successfully-uploaded": {
"message": "تم الرفع بنجاح"
@@ -237,7 +162,7 @@
"message": "{count, plural, one {# تعديل} other {# تعديلات}}"
},
"app.modal.install-to-play.required-modpack": {
"message": ُزْمَة التعديل مطلوبة"
"message": زمة التعديل مطلوبة"
},
"app.modal.install-to-play.server-requires-mods": {
"message": "يتطلب هذا الخادم تعديلات للعب. انقر فوق \"تثبيت\" لإعداد الملفات المطلوبة من Modrinth، ثم قم بتشغيله مباشرة إلى الخادم."
@@ -258,13 +183,7 @@
"message": "يلزم التحديث"
},
"app.modal.update-to-play.update-required-description": {
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى أحدث اصدار لتشغيل اللعبة."
},
"app.project.install-context.back-to-browse": {
"message": "العودة إلى الاستكشاف"
},
"app.project.install-context.install-content-to-instance": {
"message": "تثبيت المحتويات الى النموذج"
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى آخر اصدار لتشغيل اللعبة."
},
"app.settings.developer-mode-enabled": {
"message": "تم تفعيل وضع المطوّر."
@@ -290,74 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "إدارة الموارد"
},
"app.skins.add-button": {
"message": "إضافة مظهر"
},
"app.skins.add-button.drag-and-drop": {
"message": "أسحب ثم أفلت"
},
"app.skins.apply-button": {
"message": "تطبيق"
},
"app.skins.delete-button": {
"message": "حذف المظهر"
},
"app.skins.delete-modal.description": {
"message": "سيتم حذف المظهر بشكل نهائي، ولايمكن إرجاعه."
},
"app.skins.delete-modal.title": {
"message": "هل أنت متأكد من حذف المظهر هذا؟"
},
"app.skins.edit-button": {
"message": "تعديل المظهر"
},
"app.skins.modal.add-skin-button": {
"message": "إضافة المظهر"
},
"app.skins.modal.add-title": {
"message": "إضافة المظهر"
},
"app.skins.modal.cape-fallback-name": {
"message": "رداء"
},
"app.skins.modal.cape-section": {
"message": "رداء"
},
"app.skins.modal.edit-title": {
"message": "تعديل المظهر"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "عدل المظهر أولا للحفظ !"
},
"app.skins.modal.no-cape-tooltip": {
"message": "بدون رداء"
},
"app.skins.modal.none-cape-option": {
"message": "لا شيء"
},
"app.skins.modal.save-skin-button": {
"message": "حفظ المظهر"
},
"app.skins.modal.saving-tooltip": {
"message": "حفظ..."
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "أرفع المظهر أولا !"
},
"app.skins.preview.edit-button": {
"message": "تعديل المظهر"
},
"app.skins.previewing-badge": {
"message": "أستعراض"
},
"app.skins.rate-limit.title": {
"message": "تمهل !"
},
"app.skins.sign-in.title": {
"message": "الرجاء تسجيل الدخول"
},
"app.skins.title": {
"message": "اختيار المظهر"
"app.update-popup.body": {
"message": "تطبيق Modrinth v{version} جاهز للتثبيت! أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
},
"app.update-popup.body.download-complete": {
"message": "انتهى تنزيل تطبيق Modrinth v{version}. أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
@@ -377,6 +230,9 @@
"app.update-popup.download-complete": {
"message": "اكتمل التنزيل"
},
"app.update-popup.reload": {
"message": "إعادة تحميل"
},
"app.update-popup.title": {
"message": "تحديث متاح"
},
@@ -386,8 +242,14 @@
"app.update.complete-toast.title": {
"message": "تم تثبيت الإصدار {version} بنجاح!"
},
"app.world.server-modal.placeholder-address": {
"message": "مثال (example.modrinth.gg)"
"app.update.download-update": {
"message": "تنزيل التحديث"
},
"app.update.downloading-update": {
"message": "جار تنزيل التحديث ({percent}٪)"
},
"app.update.reload-to-update": {
"message": "أعد التحميل لتثبيت التحديث"
},
"app.world.server-modal.select-an-option": {
"message": "حدد خيارا"
@@ -638,9 +500,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "الذاكرة المخصَّصة"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "النافذة"
},
@@ -713,18 +572,6 @@
"instance.worlds.world_in_use": {
"message": "العالم قيد الاستخدام"
},
"minecraft-account.add-account": {
"message": "إضافة حساب"
},
"minecraft-account.label": {
"message": "حساب ماينكرافت"
},
"minecraft-account.select-account": {
"message": "اختيار الحساب"
},
"minecraft-account.sign-in": {
"message": "سجل دخولك لـ ماينكرافت"
},
"search.filter.locked.instance": {
"message": "مقدَّم من النسخة"
},
@@ -748,8 +595,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "يتم توفير المحمّل من قبل الخادم"
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "لا تعرض هذا التحذير مجددا"
}
}
+65 -629
View File
@@ -1,303 +1,24 @@
{
"app.action-bar.downloading-java": {
"message": "Stahování Javy {version}"
},
"app.action-bar.downloading-update": {
"message": "Stahování aktualizace"
},
"app.action-bar.downloads": {
"message": "Stahování"
},
"app.action-bar.hide-more-running-instances": {
"message": "Skrýt více spuštěných instancí"
},
"app.action-bar.make-primary-instance": {
"message": "Nastavit 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.reload-to-update": {
"message": "Znovu načíst pro aktualizaci"
},
"app.action-bar.show-more-running-instances": {
"message": "Zobrazit další spuštěné instance"
},
"app.action-bar.stop-instance": {
"message": "Zastavit instanci"
},
"app.action-bar.update": {
"message": "Aktualizace"
},
"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 logy"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Povolí pokročilé vykreslování, například efekty rozmazání, které mohou bez hardwarového zrychlení způsobovat problémy s výkonem."
},
"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, která se otevře po spuštění launcheru."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Domů"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Knihovna"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Výchozí stránka po spuštění"
},
"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": "Zahrne nedávné světy do sekce „Vrátit se do hry“ na domovské stránce."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Rychlý návrat do světů"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizovat launcher při spuštění Minecraftu."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Minimalizovat launcher"
},
"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": "Systémové dekorace"
},
"app.appearance-settings.select-option": {
"message": "Vyberte možnost"
},
"app.appearance-settings.show-play-time.description": {
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
},
"app.appearance-settings.show-play-time.title": {
"message": "Zobrazit odehraný čas"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Umožní 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": "Ověřovací Minecraft servery mohou být právě teď nedostupné. Zkontroluj si své připojení k internetu a zkus to znovu později."
"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": "Nelze se připojit k ověřovacím serverům"
},
"app.browse.add-servers-to-instance": {
"message": "Přidání serveru do instance"
},
"app.browse.add-to-an-instance": {
"message": "Přidat do instance"
},
"app.browse.add-to-instance": {
"message": "Přidat do instance"
},
"app.browse.add-to-instance-name": {
"message": "Přidat do {instanceName}"
},
"app.browse.added": {
"message": "Přidáno"
},
"app.browse.already-added": {
"message": "Již přidáno"
},
"app.browse.back-to-instance": {
"message": "Zpět k instanci"
},
"app.browse.discover-content": {
"message": "Objevovat obsah"
},
"app.browse.discover-servers": {
"message": "Objevovat servery"
},
"app.browse.hide-added-servers": {
"message": "Skrýt již přidané servery"
},
"app.browse.project-type.modpacks": {
"message": "Modpacky"
},
"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.content-install.no-compatible-versions": {
"message": "Žádná dostupná verze se neshoduje s verzí {compatibilityLabel}. Vyber verzi, abys ji i přesto nainstaloval/a. Závislosti nebudou automaticky nainstalovány."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Probíhá instalace modpacku..."
"message": "Připojení k autorizačním serverům se nezdařilo"
},
"app.export-modal.description-placeholder": {
"message": "Zadej 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": "Název modpacku"
},
"app.export-modal.modpack-name-placeholder": {
"message": "Název modpacku"
},
"app.export-modal.select-files-label": {
"message": "Nastavte, které soubory mají být do tohoto exportu zahrnuty"
},
"app.export-modal.version-number-label": {
"message": "Číslo verze"
},
"app.export-modal.version-number-placeholder": {
"message": "1.0.0"
"message": "Přidej popis modpacku..."
},
"app.instance.confirm-delete.admonition-body": {
"message": "Všechna data tvé instance budou trvale smazána, včetně světů, konfigurací a veškerého nainstalovaného obsahu."
},
"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": "Odstranění instance"
},
"app.instance.modpack-already-installed.body": {
"message": "Tento modpack je již 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 je již nainstalován"
},
"app.instance.modpack-already-installed.instance": {
"message": "Instance"
},
"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, které používám ve svém modpacku!"
},
"app.instance.mods.share-title": {
"message": "Sdílení obsahu modpacku"
},
"app.instance.mods.successfully-uploaded": {
"message": "Úspěšně nahráno"
},
"app.instance.worlds.add-server": {
"message": "Přidat server"
},
"app.instance.worlds.browse-servers": {
"message": "Procházet servery"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' bude **navždy smazán** a nebude možné jej obnovit."
},
"app.instance.worlds.delete-world-title": {
"message": "Jsi si jistý, že chceš trvale smazat tento svět?"
},
"app.instance.worlds.filter-modded": {
"message": "Módovaný"
},
"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 server nebo začni procházet"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Žádné servery nebo světy nebyly přidány"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' bude odstraněn z tvého seznamu i ze hry a nebude možné jej obnovit."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) bude odstraněn z tvého seznamu i ze hry a nebude možné jej obnovit."
},
"app.instance.worlds.remove-server-title": {
"message": "Opravdu chceš odstranit {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Hledat v {count} světech..."
},
"app.instance.worlds.this-server": {
"message": "tento server"
},
"app.modal.install-to-play.content-required": {
"message": "Požadovaný obsah"
"message": "Všechna data z instance budou navždy smazána, včetně světů, nastavení a všeho ostatního."
},
"app.modal.install-to-play.header": {
"message": "Nainstaluj pro hraní"
"message": "Nainstaluj ke hraní"
},
"app.modal.install-to-play.install-button": {
"message": "Instalovat"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mód} few {# módy} other {# módů}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Požadovaný 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 a poté se rovnou připoj na server."
"message": "{count, plural,one {#mód} other {#módy}}"
},
"app.modal.install-to-play.shared-instance": {
"message": "Sdílená instance"
@@ -305,26 +26,14 @@
"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": "Aktualizuj pro hraní"
"message": "Updatuj ke hraní"
},
"app.modal.update-to-play.update-required": {
"message": "Je vyžadována aktualizace"
},
"app.modal.update-to-play.update-required-description": {
"message": "Pro hraní {name} je vyžadována aktualizace. Prosím aktualizuj na nejnovější verzi, abys mohl hru spustit."
},
"app.project.install-button.already-installed": {
"message": "Tento projekt je již nainstalován"
},
"app.project.install-context.back-to-browse": {
"message": "Zpět k Objevování"
},
"app.project.install-context.install-content-to-instance": {
"message": "Nainstalovat obsah do instance"
"message": "Je nutná aktualizace ke hraní {name}. Prosím updatuj na nejnovější verzi k spuštění hry."
},
"app.settings.developer-mode-enabled": {
"message": "Vývojářský režim povolen."
@@ -350,200 +59,23 @@
"app.settings.tabs.resource-management": {
"message": "Správa zdrojů"
},
"app.skins.add-button": {
"message": "Přidat skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Přetáhni a pusť"
},
"app.skins.apply-button": {
"message": "Použít"
},
"app.skins.delete-button": {
"message": "Smazat skin"
},
"app.skins.delete-modal.description": {
"message": "Tahle akce navždy smaže vybraný skin a nejde vrátit zpět."
},
"app.skins.delete-modal.title": {
"message": "Opravdu chceš smazat tenhle skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Nepodařilo se načíst přetažený soubor."
},
"app.skins.dropped-file-error.title": {
"message": "Chyba při načítání souboru"
},
"app.skins.edit-button": {
"message": "Upravit skin"
},
"app.skins.modal.add-skin-button": {
"message": "Přidat skin"
},
"app.skins.modal.add-title": {
"message": "Přidání skinu"
},
"app.skins.modal.arm-style-section": {
"message": "Typ paží"
},
"app.skins.modal.arm-style-slim": {
"message": "Štíhlý"
},
"app.skins.modal.arm-style-wide": {
"message": "Široký"
},
"app.skins.modal.cape-fallback-name": {
"message": "Plášť"
},
"app.skins.modal.cape-section": {
"message": "Plášť"
},
"app.skins.modal.edit-title": {
"message": "Úprava skinu"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Nejdříve uprav skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Bez pláště"
},
"app.skins.modal.none-cape-option": {
"message": "Žádný"
},
"app.skins.modal.replace-texture-button": {
"message": "Nahradit texturu"
},
"app.skins.modal.save-skin-button": {
"message": "Uložit skin"
},
"app.skins.modal.saving-tooltip": {
"message": "Ukládání..."
},
"app.skins.modal.texture-section": {
"message": "Textura"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Nejdříve nahraj skin!"
},
"app.skins.preview.edit-button": {
"message": "Upravit skin"
},
"app.skins.previewing-badge": {
"message": "Náhled"
},
"app.skins.rate-limit.text": {
"message": "Měníš svůj skin příliš často. Servery Mojangu dočasně zablokovaly další požadavky. Počkej chvíli, než to zkusíš znovu."
},
"app.skins.rate-limit.title": {
"message": "Zpomal!"
},
"app.skins.reorder-error.text": {
"message": "Tvé pořadí skinů se nepodařilo uložit."
},
"app.skins.reorder-error.title": {
"message": "Nepodařilo se změnit pořadí skinů"
},
"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": "Výchozí skiny"
},
"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": "Tyto skiny jsi obdržel za přispění do sbírky Modrinth Pride behěm Pride měsíce."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Uložené skiny"
},
"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": "Přihlásit se"
},
"app.skins.sign-in.description": {
"message": "Přihlas se prosím ke svému Minecraft účtu, abys mohl/a využívat funkce pro správu skinů v aplikaci Modrinth."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Nadšený Modrinth Bot"
},
"app.skins.sign-in.title": {
"message": "Přihlas se prosím"
},
"app.skins.title": {
"message": "Výběr skinu"
},
"app.update-popup.body.download-complete": {
"message": "Stahování aplikace Modrinth v{version} bylo dokončeno. Aktualizujte ji znovunačtením nebo se automaticky aktualizuje po zavření."
},
"app.update-popup.body.linux": {
"message": "Aplikace Modrinth v{version} je k dispozici. Aktualizujte ji pomocí svého správce balíčků, abyste získali nejnovější funkce a opravy!"
},
"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": "Znovu načíst pro aktualizaci"
},
"app.update-popup.title": {
"message": "Aktualizace k dispozici"
},
"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.world.server-modal.placeholder-address": {
"message": "priklad.modrinth.gg"
"app.update.download-update": {
"message": "Stáhnout aktualizaci"
},
"app.world.server-modal.select-an-option": {
"message": "Vybrat možnost"
"app.update.downloading-update": {
"message": "Stahování aktualizace ({percent}%)"
},
"app.world.world-item.incompatible-version": {
"message": "Nekompatibilní verze {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Zatím nehráno"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
"app.update.reload-to-update": {
"message": "Restartovat aplikaci pro nainstalování aktualizace"
},
"friends.action.add-friend": {
"message": "Přidat přítele"
@@ -555,19 +87,19 @@
"message": "Poslat žádost o přátelství"
},
"friends.add-friend.title": {
"message": "Přidání přítele"
"message": "Přidákamaráda"
},
"friends.add-friend.username.description": {
"message": "Může se lišit od jejich Minecraft přezdívky!"
"message": "Může být jiné než jejich Minecraft jméno!"
},
"friends.add-friend.username.placeholder": {
"message": "Zadej přezdívku na Modrinthu..."
"message": "Zadejte uživatelské jméno Modrinth..."
},
"friends.add-friend.username.title": {
"message": "Jaká je přezdívka tvého přítele na Modrinthu?"
"message": "Jak se váš kamarád jmenuje na Modrinthu?"
},
"friends.add-friends-to-share": {
"message": "<link>Přidej si přátele</link>, abys viděl, co hrají!"
"message": "<link>Přidejte si přátelé</link> a podívejte se, co hrají!"
},
"friends.friend.cancel-request": {
"message": "Zrušit žádost"
@@ -582,7 +114,7 @@
"message": "Zobrazit profil"
},
"friends.heading": {
"message": "Přátelé"
"message": "Kamarádi"
},
"friends.heading.active": {
"message": "Aktivní"
@@ -597,7 +129,7 @@
"message": "Čeká na vyřízení"
},
"friends.no-friends-match": {
"message": "Žádní přátelé neodpovídají ''{query}''"
"message": "Žádní přátelé neodpovídají '{query}''"
},
"friends.search-friends-placeholder": {
"message": "Hledat přátele..."
@@ -606,7 +138,7 @@
"message": "{title} - {count}"
},
"friends.sign-in-to-add-friends": {
"message": "<link>Přihlas se k Modrinth účtu</link>, abys mohl přidávat přátele a vidět, co hrají!"
"message": "<link>Přihlašte se ke svému Modrinth účtu</link>, abyste jsi přidali přátele a uviděli co hrají!"
},
"instance.add-server.add-and-play": {
"message": "Přidat a hrát"
@@ -618,10 +150,10 @@
"message": "Vypnuto"
},
"instance.add-server.resource-pack.enabled": {
"message": "Povolený"
"message": "Zapnuto"
},
"instance.add-server.resource-pack.prompt": {
"message": "Zeptat se"
"message": "Ptát se"
},
"instance.add-server.title": {
"message": "Přidat server"
@@ -630,7 +162,7 @@
"message": "Upravit server"
},
"instance.edit-world.hide-from-home": {
"message": "Skrýt z domovské stránky"
"message": "Skrýt z domovské obrazovky"
},
"instance.edit-world.name": {
"message": "Název"
@@ -639,17 +171,11 @@
"message": "Minecraft svět"
},
"instance.edit-world.reset-icon": {
"message": "Obnovit ikonu"
"message": "Resetovat ikonu"
},
"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"
},
@@ -657,22 +183,22 @@
"message": "Název"
},
"instance.server-modal.placeholder-name": {
"message": "Minecraft server"
"message": "Minecraft Server"
},
"instance.server-modal.resource-pack": {
"message": "Balíček textur"
"message": "Balíčky textur"
},
"instance.settings.tabs.general": {
"message": "Obecné"
},
"instance.settings.tabs.general.delete": {
"message": "Odstranění instance"
"message": "Odstranit instalaci"
},
"instance.settings.tabs.general.delete.button": {
"message": "Odstranit instanci"
"message": "Odstranit instalaci"
},
"instance.settings.tabs.general.delete.description": {
"message": "Trvale smaže instanci z tvého zařízení, včetně světů, konfigurací a veškerého nainstalovaného obsahu. Buď opatrný, jakmile instanci smažeš, nepůjde ji obnovit."
"message": "Trvale smaže instalaci z vašeho zařízení, včetně vašich světů, konfigurací a veškerého nainstalovaného obsahu. Buďte opatrní, protože jakmile instalaci smažete, nebude možné ji obnovit."
},
"instance.settings.tabs.general.deleting.button": {
"message": "Mazání..."
@@ -681,64 +207,40 @@
"message": "Duplikovat"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Nelze duplikovat během instalace."
"message": "Nelze duplikovat při instalaci."
},
"instance.settings.tabs.general.duplicate-instance": {
"message": "Duplikace instance"
"message": "Duplikovat instalaci"
},
"instance.settings.tabs.general.duplicate-instance.description": {
"message": "Vytvoří kopii této instance, včetně světů, konfigurací, modů, atd."
"message": "Vytvoří kopii této instalace, včetně světů, konfigurací, modů atd."
},
"instance.settings.tabs.general.edit-icon": {
"message": "Upravit ikonu"
},
"instance.settings.tabs.general.edit-icon.remove": {
"message": "Odebrat ikonu"
"message": "Odstranit ikonu"
},
"instance.settings.tabs.general.edit-icon.replace": {
"message": "Nahradit ikonu"
"message": "Změnit ikonu"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Vybrat ikonu"
},
"instance.settings.tabs.general.library-groups": {
"message": "Skupiny knihovny"
"message": "Skupina knihoven"
},
"instance.settings.tabs.general.library-groups.create": {
"message": "Vytvořit novou skupinu"
},
"instance.settings.tabs.general.library-groups.description": {
"message": "Skupiny knihovny ti umožňují uspořádat instance do různých sekcí v knihovně."
"message": "Skupiny knihoven vám umožňují uspořádat instalace do různých sekcí."
},
"instance.settings.tabs.general.library-groups.enter-name": {
"message": "Zadej název skupiny"
"message": "Zadejte název skupiny"
},
"instance.settings.tabs.general.name": {
"message": "Jméno"
},
"instance.settings.tabs.general.update-channel": {
"message": "Aktualizační kanál"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Vydané, beta a alfa verze se zobrazí jako dostupné aktualizace."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Vydané a beta verze se zobrazí jako dostupné aktualizace."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Vydání"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Pouze vydané verze se zobrazí jako dostupné aktualizace."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Vyber aktualizační kanál"
"message": "Název"
},
"instance.settings.tabs.hooks": {
"message": "Spouštěcí hooky"
@@ -747,28 +249,28 @@
"message": "Vlastní spouštěcí hooky"
},
"instance.settings.tabs.hooks.description": {
"message": "Hooky umožňují pokročilým uživatelům spustit určité systémové příkazy před a po spuštění hry."
"message": "Hooky umožňují pokročilým uživatelům spouštět určité systémové příkazy před a po spuštění hry."
},
"instance.settings.tabs.hooks.post-exit": {
"message": "Po ukončení"
},
"instance.settings.tabs.hooks.post-exit.description": {
"message": "Provede se po ukončení hry."
"message": "Spušteno po vypnutí hry."
},
"instance.settings.tabs.hooks.post-exit.enter": {
"message": "Zadej příkaz k provedení po ukončení..."
"message": "Zadejte příkaz k provedení po ukončení..."
},
"instance.settings.tabs.hooks.pre-launch": {
"message": "Před spuštěním"
},
"instance.settings.tabs.hooks.pre-launch.description": {
"message": "Provede se před spuštěním instance."
"message": "Spouštějí se před spuštěním instalace."
},
"instance.settings.tabs.hooks.pre-launch.enter": {
"message": "Zadej příkaz k provedení před spuštěním..."
"message": "Zadejte příkaz k provedení před spuštěním..."
},
"instance.settings.tabs.hooks.title": {
"message": "Spouštěcí příkazy hry"
"message": "Spouštěcí hooky hry"
},
"instance.settings.tabs.hooks.wrapper": {
"message": "Wrapper"
@@ -777,7 +279,7 @@
"message": "Příkaz wrapperu pro spuštění Minecraftu."
},
"instance.settings.tabs.hooks.wrapper.enter": {
"message": "Zadej příkaz wrapperu..."
"message": "Zadejte příkaz wrapperu..."
},
"instance.settings.tabs.installation": {
"message": "Instalace"
@@ -788,24 +290,6 @@
"instance.settings.tabs.java": {
"message": "Java a pamět"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Vlastní proměnná prostředí"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Vlastní argumenty Javy"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Vlastní instalace Javy"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Vlastní nastavení paměti"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Zadej proměnná prostředí..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Zadej argumenty Javy..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Proměnné prostředí"
},
@@ -813,16 +297,13 @@
"message": "Hooky"
},
"instance.settings.tabs.java.java-arguments": {
"message": "Argumenty Javy"
"message": "Java argumenty"
},
"instance.settings.tabs.java.java-installation": {
"message": "Instalace Javy"
},
"instance.settings.tabs.java.java-memory": {
"message": "Přiřazeno paměti"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
"message": "Paměti přiřazeno"
},
"instance.settings.tabs.window": {
"message": "Okno"
@@ -831,10 +312,10 @@
"message": "Vlastní nastavení okna"
},
"instance.settings.tabs.window.fullscreen": {
"message": "Režim celé obrazovky"
"message": "Celá obrazovka"
},
"instance.settings.tabs.window.fullscreen.description": {
"message": "Při spuštění otevřít hru v režimu celé obrazovky (použitím options.txt)."
"message": "Spustit hru v plné obrazovce pokud spuštěno (použitím options.txt)."
},
"instance.settings.tabs.window.height": {
"message": "Výška"
@@ -843,7 +324,7 @@
"message": "Výška herního okna při spuštění."
},
"instance.settings.tabs.window.height.enter": {
"message": "Zadej výšku..."
"message": "Zadejte výšku..."
},
"instance.settings.tabs.window.width": {
"message": "Šířka"
@@ -852,19 +333,19 @@
"message": "Šířka herního okna při spuštění."
},
"instance.settings.tabs.window.width.enter": {
"message": "Zadej šířku..."
"message": "Zadejte šířku..."
},
"instance.worlds.a_minecraft_server": {
"message": "Minecraft server"
"message": "Minecraft Server"
},
"instance.worlds.cant_connect": {
"message": "Nelze se připojit k serveru"
"message": "Nelze připojit k serveru"
},
"instance.worlds.copy_address": {
"message": "Kopírovat adresu"
},
"instance.worlds.dont_show_on_home": {
"message": "Nezobrazovat na domovské stránce"
"message": "Nezobrazuje na domovské stránce"
},
"instance.worlds.game_already_open": {
"message": "Instance je již otevřená"
@@ -875,17 +356,14 @@
"instance.worlds.incompatible_server": {
"message": "Server je nekompatibilní"
},
"instance.worlds.linked_server": {
"message": "Spravováno projektem serveru"
},
"instance.worlds.no_contact": {
"message": "Se serverem se nepodařilo spojit"
"message": "Server se nepodařilo spojit"
},
"instance.worlds.no_server_quick_play": {
"message": "Připojit se rovnou k serverům můžeš pouze s Minecraft verzí Alpha 1.0.5+"
"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ětů pro jednoho hráče pouze s Minecraft verzí 1.20+"
"message": "Můžeš se rovnou připojit do světa jednoho hráče pouze v Minecraftu 1.20+"
},
"instance.worlds.play_instance": {
"message": "Hrát instanci"
@@ -894,69 +372,27 @@
"message": "Zobrazit instanci"
},
"instance.worlds.world_in_use": {
"message": "Svět je používán"
},
"minecraft-account.add-account": {
"message": "Přidat účet"
},
"minecraft-account.label": {
"message": "Minecraft účet"
},
"minecraft-account.not-signed-in": {
"message": "Nejste přihlášeni"
},
"minecraft-account.remove-account": {
"message": "Odebrat účet"
},
"minecraft-account.select-account": {
"message": "Zvolte účet"
},
"minecraft-account.sign-in": {
"message": "Přihlásit se do Minecraftu"
"message": "Svět je použit"
},
"search.filter.locked.instance": {
"message": "Poskytováno instancí"
},
"search.filter.locked.instance-game-version.title": {
"message": "Verze hry je poskytována instancí"
"message": "Verze hry je poskytnut instanci"
},
"search.filter.locked.instance-loader.title": {
"message": "Načítací modul je poskytován instancí"
"message": "Spouštěč je poskytnut instancí"
},
"search.filter.locked.instance.sync": {
"message": "Synchronizováno s instancí"
},
"search.filter.locked.server": {
"message": "Poskytováno serverem"
"message": "Synchronizováno z instancí"
},
"search.filter.locked.server-environment.title": {
"message": "Pouze módy ze strany klientu mohou být přidány na server"
"message": "Jen klientské mody mohou být přidány na server"
},
"search.filter.locked.server-game-version.title": {
"message": "Verze hry je poskytována serverem"
"message": "Verzi hry poskytuje server"
},
"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, bez ohledu na jeho formát (včetně formátu .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Toto varování znovu 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"
}
}
+15 -384
View File
@@ -1,127 +1,10 @@
{
"app.action-bar.downloading-java": {
"message": "Downloader Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Downloader opdatering"
},
"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.reload-to-update": {
"message": "Genindlæs for at opdater"
},
"app.action-bar.show-more-running-instances": {
"message": "Vis flere kørende instances"
},
"app.action-bar.stop-instance": {
"message": "Stop instance"
},
"app.action-bar.update": {
"message": "Opdater"
},
"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}"
},
@@ -131,9 +14,6 @@
"app.browse.already-added": {
"message": "Allerede tilføjet"
},
"app.browse.back-to-instance": {
"message": "Tilbage til instance"
},
"app.browse.discover-content": {
"message": "Opdag indhold"
},
@@ -141,25 +21,10 @@
"message": "Opdag servere"
},
"app.browse.hide-added-servers": {
"message": "Gem allerede tilføjet til servere"
"message": "Gem tilføjet 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.content-install.no-compatible-versions": {
"message": "Ingen tilgængelige versioner passer til {compatibilityLabel}. Vælg en version for at installere. Afhængigheder bliver ikke installeret automatisk."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Installere modpack..."
"app.browse.hide-installed-content": {
"message": "Gem installeret indhold"
},
"app.export-modal.description-placeholder": {
"message": "Indtast modpack beskrivelse..."
@@ -170,9 +35,6 @@
"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"
},
@@ -180,7 +42,7 @@
"message": "Modpack navn"
},
"app.export-modal.select-files-label": {
"message": "Konfigurer hvilke filer inkludere i denne eksport"
"message": "Vælg filer og mapper til at inkludere i pakken"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -206,12 +68,6 @@
"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"
},
@@ -230,42 +86,6 @@
"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"
},
@@ -302,15 +122,6 @@
"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."
},
@@ -335,137 +146,8 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcestyring"
},
"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": {
"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.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."
@@ -485,6 +167,9 @@
"app.update-popup.download-complete": {
"message": "Download færdiggjort"
},
"app.update-popup.reload": {
"message": "Geninlæs"
},
"app.update-popup.title": {
"message": "Opdatering tilgængelig"
},
@@ -494,23 +179,14 @@
"app.update.complete-toast.title": {
"message": "Version {version} var installeret med succes!"
},
"app.world.server-modal.placeholder-address": {
"message": "eksemple.modrinth.gg"
"app.update.download-update": {
"message": "Download opdatering"
},
"app.world.server-modal.select-an-option": {
"message": "Vælg en mulighed"
"app.update.downloading-update": {
"message": "Downloader opdatering ({percent}%)"
},
"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"
"app.update.reload-to-update": {
"message": "Genindlæs for at installere opdatering"
},
"friends.action.add-friend": {
"message": "Tilføj en ven"
@@ -611,12 +287,6 @@
"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"
},
@@ -731,9 +401,6 @@
"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"
},
@@ -749,9 +416,6 @@
"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"
},
@@ -824,24 +488,6 @@
"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"
},
@@ -865,20 +511,5 @@
},
"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"
}
}
+30 -381
View File
@@ -1,129 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Java {version} wird heruntergeladen"
},
"app.action-bar.downloading-update": {
"message": "Update 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.reload-to-update": {
"message": "Zum Aktualisieren neu laden"
},
"app.action-bar.show-more-running-instances": {
"message": "Weitere laufende Instanzen anzeigen"
},
"app.action-bar.stop-instance": {
"message": "Instanz stoppen"
},
"app.action-bar.update": {
"message": "Aktualisieren"
},
"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-servers-to-instance": {
"app.browse.add-server-to-instance": {
"message": "Server zu Instanz hinzufügen"
},
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zu Instanz hinzufügen"
@@ -137,9 +23,6 @@
"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"
},
@@ -147,25 +30,13 @@
"message": "Server entdecken"
},
"app.browse.hide-added-servers": {
"message": "Bereits hinzugefügte Server ausblenden"
"message": "Hinzugefügte Server ausblenden"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"app.browse.hide-installed-content": {
"message": "Installierte Inhalte ausblenden"
},
"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.browse.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.export-modal.description-placeholder": {
"message": "Modpaketbeschreibung eingeben..."
@@ -176,9 +47,6 @@
"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"
},
@@ -186,7 +54,7 @@
"message": "Modpaketname"
},
"app.export-modal.select-files-label": {
"message": "Konfiguriere, welche Dateien in diesem Export miteinbezogen werden"
"message": "Wähle Dateien und Ordner zum hinzufügen im Paket aus"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -242,6 +110,9 @@
"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?"
},
@@ -263,6 +134,12 @@
"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?"
},
@@ -308,15 +185,6 @@
"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."
},
@@ -341,152 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcenmanagement"
},
"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.reorder-error.text": {
"message": "Deine Skin-Reihenfolge konnte nicht gespeichert werden."
},
"app.skins.reorder-error.title": {
"message": "Skins konnten nicht neu angeordnet werden"
},
"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": {
"message": "Modrinth App v{version} ist bereit zur Installation! Lade die App neu um jetzt zu aktualisieren, oder automatisch nach dem schliessen der Modrinth App."
},
"app.update-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."
@@ -507,7 +231,7 @@
"message": "Download abgeschlossen"
},
"app.update-popup.reload": {
"message": "Zum Aktualisieren neu laden"
"message": "Neu Laden"
},
"app.update-popup.title": {
"message": "Aktualisierung verfügbar"
@@ -518,6 +242,15 @@
"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"
},
@@ -707,30 +440,6 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Update-Kanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Release-, Beta- und Alpha-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Release- und Beta-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Release"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Nur Release-Versionen werden als verfügbare Updates angezeigt."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Update-Kanal auswählen"
},
"instance.settings.tabs.hooks": {
"message": "Start Hooks"
},
@@ -779,24 +488,6 @@
"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"
},
@@ -812,9 +503,6 @@
"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"
},
@@ -887,24 +575,6 @@
"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"
},
@@ -928,26 +598,5 @@
},
"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"
}
}
+33 -384
View File
@@ -1,129 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Java {version} wird heruntergeladen"
},
"app.action-bar.downloading-update": {
"message": "Update 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 laufenden Instanzen"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primäre Instanz"
},
"app.action-bar.reload-to-update": {
"message": "Zum Aktualisieren neu laden"
},
"app.action-bar.show-more-running-instances": {
"message": "Weitere laufende Instanzen anzeigen"
},
"app.action-bar.stop-instance": {
"message": "Instanz stoppen"
},
"app.action-bar.update": {
"message": "Aktualisieren"
},
"app.action-bar.view-active-downloads": {
"message": "Aktive Downloads anzeigen"
},
"app.action-bar.view-instance": {
"message": "Instanz anzeigen"
},
"app.action-bar.view-logs": {
"message": "Logs 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-servers-to-instance": {
"message": "Server zu Instanz hinzufügen"
"app.browse.add-server-to-instance": {
"message": "Server zur Instanz hinzufügen"
},
"app.browse.add-to-an-instance": {
"message": "Zu Instanz hinzufügen"
"app.browse.add-servers-to-instance": {
"message": "Server zu deiner Instanz hinzufügen"
},
"app.browse.add-to-instance": {
"message": "Zur Instanz hinzufügen"
@@ -137,9 +23,6 @@
"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"
},
@@ -147,25 +30,13 @@
"message": "Server entdecken"
},
"app.browse.hide-added-servers": {
"message": "Bereits hinzugefügte Server ausblenden"
"message": "Hinzugefügte Server ausblenden"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"app.browse.hide-installed-content": {
"message": "Installierte Inhalte ausblenden"
},
"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.browse.install-content-to-instance": {
"message": "Inhalt in Instanz installieren"
},
"app.export-modal.description-placeholder": {
"message": "Beschreibung des Modpacks eingeben..."
@@ -176,9 +47,6 @@
"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"
},
@@ -186,7 +54,7 @@
"message": "Modpackname"
},
"app.export-modal.select-files-label": {
"message": "Konfiguriere, welche Dateien in diesen Export enthalten sind"
"message": "Wähle Dateien und Ordner aus, die in das Paket sollen"
},
"app.export-modal.version-number-label": {
"message": "Versionsnummer"
@@ -195,7 +63,7 @@
"message": "1.0.0"
},
"app.instance.confirm-delete.admonition-body": {
"message": "Alle Daten deiner Instanz werden permanent gelöscht, einschließlich deiner Welten, Konfigurationen und allen installierten Inhalten."
"message": "Alle Daten für deine 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"
@@ -242,6 +110,9 @@
"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?"
},
@@ -263,6 +134,12 @@
"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?"
},
@@ -308,15 +185,6 @@
"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."
},
@@ -341,152 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "Ressourcenmanagement"
},
"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 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": "Langsamer!"
},
"app.skins.reorder-error.text": {
"message": "Die Anordnung deiner Skins konnte nicht gespeichert werden."
},
"app.skins.reorder-error.title": {
"message": "Skins konnten nicht neu angeordnet werden"
},
"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": {
"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.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."
@@ -507,7 +231,7 @@
"message": "Download abgeschlossen"
},
"app.update-popup.reload": {
"message": "Zum Aktualisieren neu laden"
"message": "Neu laden"
},
"app.update-popup.title": {
"message": "Aktualisierung verfügbar"
@@ -518,6 +242,15 @@
"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"
},
@@ -669,7 +402,7 @@
"message": "Wird gelöscht..."
},
"instance.settings.tabs.general.duplicate-button": {
"message": "Duplizieren"
"message": "Kopieren"
},
"instance.settings.tabs.general.duplicate-button.tooltip.installing": {
"message": "Kann während der Installation nicht dupliziert werden."
@@ -707,30 +440,6 @@
"instance.settings.tabs.general.name": {
"message": "Name"
},
"instance.settings.tabs.general.update-channel": {
"message": "Aktualisierungskanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Voll, Beta- und Alpha-Versionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Voll- und Beta-Versionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Vollversion"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Nur Vollversionen werden als verfügbare Aktualisierungen angezeigt."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Aktualisierungskanal auswählen"
},
"instance.settings.tabs.hooks": {
"message": "Startargumente"
},
@@ -779,24 +488,6 @@
"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"
},
@@ -812,9 +503,6 @@
"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"
},
@@ -887,24 +575,6 @@
"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"
},
@@ -928,26 +598,5 @@
},
"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"
}
}
+22 -382
View File
@@ -1,129 +1,15 @@
{
"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-servers-to-instance": {
"message": "Adding server to instance"
"app.browse.add-server-to-instance": {
"message": "Add server to instance"
},
"app.browse.add-to-an-instance": {
"message": "Add to an instance"
"app.browse.add-servers-to-instance": {
"message": "Add servers to your instance"
},
"app.browse.add-to-instance": {
"message": "Add to instance"
@@ -137,9 +23,6 @@
"app.browse.already-added": {
"message": "Already added"
},
"app.browse.back-to-instance": {
"message": "Back to instance"
},
"app.browse.discover-content": {
"message": "Discover content"
},
@@ -147,25 +30,13 @@
"message": "Discover servers"
},
"app.browse.hide-added-servers": {
"message": "Hide already added servers"
"message": "Hide added servers"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"app.browse.hide-installed-content": {
"message": "Hide installed content"
},
"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.browse.install-content-to-instance": {
"message": "Install content to instance"
},
"app.export-modal.description-placeholder": {
"message": "Enter modpack description..."
@@ -176,9 +47,6 @@
"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"
},
@@ -186,7 +54,7 @@
"message": "Modpack name"
},
"app.export-modal.select-files-label": {
"message": "Configure which files are included in this export"
"message": "Select files and folders to include in pack"
},
"app.export-modal.version-number-label": {
"message": "Version number"
@@ -317,15 +185,6 @@
"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."
},
@@ -350,152 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "Resource management"
},
"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.reorder-error.text": {
"message": "Your skin order could not be saved."
},
"app.skins.reorder-error.title": {
"message": "Failed to reorder skins"
},
"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": {
"message": "Modrinth App v{version} is ready to install! Reload to update now, or automatically when you close Modrinth App."
},
"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."
@@ -516,7 +231,7 @@
"message": "Download complete"
},
"app.update-popup.reload": {
"message": "Reload to update"
"message": "Reload"
},
"app.update-popup.title": {
"message": "Update available"
@@ -527,6 +242,15 @@
"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"
},
@@ -716,30 +440,6 @@
"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"
},
@@ -788,24 +488,6 @@
"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"
},
@@ -821,9 +503,6 @@
"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"
},
@@ -896,24 +575,6 @@
"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"
},
@@ -937,26 +598,5 @@
},
"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 its 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"
}
}
+35 -386
View File
@@ -1,129 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Descargando Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Descargando actualización"
},
"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.reload-to-update": {
"message": "Reinicia para actualizar"
},
"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.update": {
"message": "Actualizar"
},
"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-servers-to-instance": {
"message": "Añadiendo servidor a instancia"
"app.browse.add-server-to-instance": {
"message": "Añadir servidor a instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
"app.browse.add-servers-to-instance": {
"message": "Añade servidores a tu instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a instancia"
@@ -137,9 +23,6 @@
"app.browse.already-added": {
"message": "Ya se ha añadido"
},
"app.browse.back-to-instance": {
"message": "Volver a instancia"
},
"app.browse.discover-content": {
"message": "Descubrir contenido"
},
@@ -147,25 +30,13 @@
"message": "Descubrir servidores"
},
"app.browse.hide-added-servers": {
"message": "Ocultar servidores ya añadidos"
"message": "Ocultar servidores añadidos"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"app.browse.hide-installed-content": {
"message": "Ocultar contenido instalado"
},
"app.browse.server-instance-content-warning": {
"message": "Añadir contenido puede romper la compatibilidad cuando te unes al servidor. Cualquier contenido añadido tambien se perderá cuando actualizes el contenido en la instancia del servidor."
},
"app.browse.server.installing": {
"message": "Instalando"
},
"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.browse.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
},
"app.export-modal.description-placeholder": {
"message": "Introduce la descripción del modpack..."
@@ -176,9 +47,6 @@
"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"
},
@@ -186,7 +54,7 @@
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Configura que archivos incluir en esta exportación"
"message": "Selecciona archivos y carpetas para incluirlos en el pack"
},
"app.export-modal.version-number-label": {
"message": "Número de la versión"
@@ -228,7 +96,7 @@
"message": "Se añadieron {count} proyectos"
},
"app.instance.mods.share-text": {
"message": "¡Mira los proyectos que estoy usando en mi modpack!"
"message": "¡Mira a los proyectos que estoy usando en mi modpack!"
},
"app.instance.mods.share-title": {
"message": "Compartiendo contenido del modpack"
@@ -242,6 +110,9 @@
"app.instance.worlds.browse-servers": {
"message": "Explorar servidores"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" se **eliminará permanentemente** y no habrá forma de recuperarlo."
},
"app.instance.worlds.delete-world-title": {
"message": "¿Estás seguro de que quieres eliminar este mundo de forma permanente?"
},
@@ -263,6 +134,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "No hay servidores ni mundos añadidos"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) se eliminará de tu lista, también dentro del juego, y no podrás recuperarlo."
},
"app.instance.worlds.remove-server-title": {
"message": "¿Estás seguro de que quieres eliminar {name}?"
},
@@ -276,7 +153,7 @@
"message": "Contenido requerido"
},
"app.modal.install-to-play.header": {
"message": "Instalar para jugar"
"message": "Instala para jugar"
},
"app.modal.install-to-play.install-button": {
"message": "Instalar"
@@ -300,7 +177,7 @@
"message": "Ver contenidos"
},
"app.modal.update-to-play.header": {
"message": "Actualizar para jugar"
"message": "Actualiza para jugar"
},
"app.modal.update-to-play.update-required": {
"message": "Actualización requerida"
@@ -308,15 +185,6 @@
"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 al explorador"
},
"app.project.install-context.install-content-to-instance": {
"message": "Instalar contenido a la instancia"
},
"app.settings.developer-mode-enabled": {
"message": "Modo desarrollador activado."
},
@@ -341,152 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "Gestión de recursos"
},
"app.skins.add-button": {
"message": "Añadir skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Arrastra y suelta"
},
"app.skins.apply-button": {
"message": "Aplicar"
},
"app.skins.delete-button": {
"message": "Eliminar skin"
},
"app.skins.delete-modal.description": {
"message": "Esto eliminará permanentemente la skin seleccionada. Esta acción no se puede deshacer."
},
"app.skins.delete-modal.title": {
"message": "¿Estás seguro de que quieres eliminar esta 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": "¡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 una 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.reorder-error.text": {
"message": "No se ha podido guardar el orden de las skins."
},
"app.skins.reorder-error.title": {
"message": "Error al reordenar las skins"
},
"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 guardadas"
},
"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": {
"message": "¡Modrinth App v{version} está lista para instalarse! Actualiza ahora o automáticamente al cerrar la Modrinth App."
},
"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."
@@ -507,7 +231,7 @@
"message": "Descarga completada"
},
"app.update-popup.reload": {
"message": "Reinicia para actualizar"
"message": "Recargar"
},
"app.update-popup.title": {
"message": "Actualización disponible"
@@ -518,6 +242,15 @@
"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"
},
@@ -707,30 +440,6 @@
"instance.settings.tabs.general.name": {
"message": "Nombre"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de actualizaciones"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Las versiones estables, beta y alpha se mostrarán como actualizaciones disponibles. "
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Las versiones estables y beta serán mostradas como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Estable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Sólo las versiones estables serán mostradas como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Selecciona el canal de actualizaciones"
},
"instance.settings.tabs.hooks": {
"message": "Hooks de inicio"
},
@@ -779,24 +488,6 @@
"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"
},
@@ -812,9 +503,6 @@
"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"
},
@@ -887,24 +575,6 @@
"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"
},
@@ -921,33 +591,12 @@
"message": "Proporcionado por el servidor"
},
"search.filter.locked.server-environment.title": {
"message": "Solo se pueden añadir mods que sean del lado del cliente a la instancia del servidor"
"message": "Solo se puede añadir mods que sean del lado del cliente a la instancia del servidor"
},
"search.filter.locked.server-game-version.title": {
"message": "La versión del juego es proporcionada por el servidor"
},
"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"
}
}
+25 -385
View File
@@ -1,129 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Descargando Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Descargando actualización"
},
"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.reload-to-update": {
"message": "Recargar para actualizar"
},
"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.update": {
"message": "Actualizar"
},
"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 en la que el launcher inicia."
},
"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-servers-to-instance": {
"message": "Añadiendo servidor a la instancia"
"app.browse.add-server-to-instance": {
"message": "Añadir servidor a la instancia"
},
"app.browse.add-to-an-instance": {
"message": "Añadir a una instancia"
"app.browse.add-servers-to-instance": {
"message": "Añadir servidor a tu instancia"
},
"app.browse.add-to-instance": {
"message": "Añadir a la instancia"
@@ -137,9 +23,6 @@
"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"
},
@@ -147,25 +30,13 @@
"message": "Descubrir servidores"
},
"app.browse.hide-added-servers": {
"message": "Ocultar servidores ya añadidos"
"message": "Ocultar servidores añadidos"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"app.browse.hide-installed-content": {
"message": "Ocultar contenido instalado"
},
"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.browse.install-content-to-instance": {
"message": "Instalar contenido a una instancia"
},
"app.export-modal.description-placeholder": {
"message": "Escribe la descripción del modpack..."
@@ -176,9 +47,6 @@
"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"
},
@@ -186,7 +54,7 @@
"message": "Nombre del modpack"
},
"app.export-modal.select-files-label": {
"message": "Configura que archivos se incluirán en esta instancia"
"message": "Seleccione archivos y carpetas para incluir en el paquete"
},
"app.export-modal.version-number-label": {
"message": "Número de versión"
@@ -297,7 +165,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"
@@ -317,15 +185,6 @@
"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."
},
@@ -350,152 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "Gestión de recursos"
},
"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.reorder-error.text": {
"message": "No se ha podido guardar el orden de las skins."
},
"app.skins.reorder-error.title": {
"message": "Error al reordenar las skins"
},
"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": {
"message": "¡La versión v{version} de Modrinth está lista para instalarse! Actualiza ahora o automáticamente al cerrar la aplicación."
},
"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."
@@ -516,7 +231,7 @@
"message": "Descarga completada"
},
"app.update-popup.reload": {
"message": "Recargar para actualizar"
"message": "Recargar"
},
"app.update-popup.title": {
"message": "Actualización disponible"
@@ -527,6 +242,15 @@
"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"
},
@@ -716,30 +440,6 @@
"instance.settings.tabs.general.name": {
"message": "Nombre"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de actualizaciones"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Se mostrarán las versiones estables, beta y alfa como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Se mostrarán las versiones estables y beta como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Estable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Solo se mostrarán las versiones estables como actualizaciones disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Seleccionar canal de actualizaciones"
},
"instance.settings.tabs.hooks": {
"message": "Hooks de lanzamiento"
},
@@ -788,24 +488,6 @@
"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"
},
@@ -821,9 +503,6 @@
"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"
},
@@ -896,24 +575,6 @@
"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"
},
@@ -921,7 +582,7 @@
"message": "Versión del juego proporciona por la instancia"
},
"search.filter.locked.instance-loader.title": {
"message": "Loader proporcionado por la instancia"
"message": "Cargador proporcionado por la instancia"
},
"search.filter.locked.instance.sync": {
"message": "Sincronizar con la instancia"
@@ -936,27 +597,6 @@
"message": "La versión del juego es proporcionada por el servidor"
},
"search.filter.locked.server-loader.title": {
"message": "Loader 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 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"
"message": "El cargador lo proporciona el servidor"
}
}
+13 -361
View File
@@ -1,177 +1,15 @@
{
"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.content-install.no-compatible-versions": {
"message": "Yhtään versiota ei ole saatavilla yhteensopivuudella {compatibilityLabel}. Valitse asennettava versio siitä huolimatta. Riippuvuuksia ei asenneta automaattisesti."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Asennetaan modipakettia..."
},
"app.export-modal.description-placeholder": {
"message": "Lisää modipaketin kuvaus..."
},
"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": "Modipaketin nimi"
"message": "Modpack nimi"
},
"app.export-modal.select-files-label": {
"message": "Muuta mitkä tiedostot sisältyvät tähän vientiin"
"message": "Valitse tiedostot ja kansiot pakettiin"
},
"app.export-modal.version-number-label": {
"message": "Versio numero"
@@ -179,93 +17,12 @@
"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"
},
@@ -276,7 +33,7 @@
"message": "Asenna"
},
"app.modal.install-to-play.mod-count": {
"message": "{count, plural, one {# mod} other {# modia}}"
"message": "one {{count, plural, one {# mod} other {# modia}}"
},
"app.modal.install-to-play.required-modpack": {
"message": "Vaadittu modipaketti"
@@ -302,15 +59,6 @@
"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ä."
},
@@ -335,20 +83,8 @@
"app.settings.tabs.resource-management": {
"message": "Resurssien hallinta"
},
"app.skins.reorder-error.title": {
"message": "Skinien järjestyksen muuttaminen epäonnistui"
},
"app.skins.section.mounts-of-mayhem": {
"message": "Sekasorron ratsut"
},
"app.skins.section.tiny-takeover": {
"message": "Pienten vallankumous"
},
"app.skins.sign-in.title": {
"message": "Kirjaudu sisään"
},
"app.skins.title": {
"message": "Skinivalinta"
"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."
@@ -369,7 +105,7 @@
"message": "Lataus valmis"
},
"app.update-popup.reload": {
"message": "Lataa uudelleen päivittääksesi"
"message": "Lataa uudelleen"
},
"app.update-popup.title": {
"message": "Päivitys saatavilla"
@@ -380,23 +116,14 @@
"app.update.complete-toast.title": {
"message": "Versio {version} asennettiin onnistuneesti!"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
"app.update.download-update": {
"message": "Lataa päivitys"
},
"app.world.server-modal.select-an-option": {
"message": "Valitse vaihtoehto"
"app.update.downloading-update": {
"message": "Ladataan päivitystä ({percent}%)"
},
"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"
"app.update.reload-to-update": {
"message": "Lataa uudelleen asentaaksesi päivityksen"
},
"friends.action.add-friend": {
"message": "Lisää ystävä"
@@ -497,12 +224,6 @@
"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"
},
@@ -569,15 +290,6 @@
"instance.settings.tabs.general.name": {
"message": "Nimi"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Julkaisu"
},
"instance.settings.tabs.hooks": {
"message": "Aloitustoiminnot"
},
@@ -626,24 +338,6 @@
"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"
},
@@ -659,9 +353,6 @@
"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"
},
@@ -734,24 +425,6 @@
"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"
},
@@ -768,33 +441,12 @@
"message": "Palvelimen tarjoama"
},
"search.filter.locked.server-environment.title": {
"message": "Voit lisätä vain paikallisia modeja palvelin instanssiin"
"message": "Voit lisätä vain paikallisia modeja palvelinasennukseen"
},
"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"
}
}
+21 -273
View File
@@ -1,126 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Dina-download ang Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Dina-download ang update"
},
"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.reload-to-update": {
"message": "Mag-reload upang ma-update"
},
"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.update": {
"message": "Mag-update"
},
"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.show-play-time.description": {
"message": "Magpapakita ang katagal ng iyong paglalaro sa isang instansiya."
},
"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-server-to-instance": {
"message": "Idagdag ang server sa instansiya"
},
"app.browse.add-to-an-instance": {
"message": "Idagdag sa instansiya"
"app.browse.add-servers-to-instance": {
"message": "Idagdag ang mga server sa iyong instansiya"
},
"app.browse.add-to-instance": {
"message": "Idagdag sa instansiya"
@@ -134,9 +23,6 @@
"app.browse.already-added": {
"message": "Nadagdag na"
},
"app.browse.back-to-instance": {
"message": "Bumalik sa instansiya"
},
"app.browse.discover-content": {
"message": "Tumuklas ng kontento"
},
@@ -144,22 +30,13 @@
"message": "Tumuklas ng mga server"
},
"app.browse.hide-added-servers": {
"message": "Itago ang mga nailagay na mga servers"
"message": "Taguin ang mga nadagdag na server"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"app.browse.hide-installed-content": {
"message": "Taguin ang mga na-install na kontento"
},
"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.browse.install-content-to-instance": {
"message": "I-install ang kontento sa instansiya"
},
"app.export-modal.description-placeholder": {
"message": "Ilagay ang paglalarawan ng modpack..."
@@ -170,9 +47,6 @@
"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"
},
@@ -180,7 +54,7 @@
"message": "Pangalan ng modpack"
},
"app.export-modal.select-files-label": {
"message": "Isaayos kung anong mga file ang isasali sa pagluwas"
"message": "Pumili ng mga talaksan at folder na isasali sa pack"
},
"app.export-modal.version-number-label": {
"message": "Numero ng bersiyon"
@@ -200,15 +74,9 @@
"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"
},
@@ -221,9 +89,6 @@
"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"
},
@@ -233,15 +98,6 @@
"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"
},
@@ -254,27 +110,6 @@
"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"
},
@@ -311,12 +146,6 @@
"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": "Binuksan ang moda ng nagdi-develop."
},
@@ -341,35 +170,8 @@
"app.settings.tabs.resource-management": {
"message": "Pamamahala ng paglalaan"
},
"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": {
"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.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."
@@ -389,6 +191,9 @@
"app.update-popup.download-complete": {
"message": "Nakumpleto ang pagdownload"
},
"app.update-popup.reload": {
"message": "Mag-reload"
},
"app.update-popup.title": {
"message": "May bagong update"
},
@@ -398,24 +203,18 @@
"app.update.complete-toast.title": {
"message": "Tagumpay na na-install ang bersiyong {version}!"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
"app.update.download-update": {
"message": "I-download ang update"
},
"app.world.server-modal.select-an-option": {
"message": "Pumili ng opsiyon"
"app.update.downloading-update": {
"message": "Nagdadownload ng update ({percent}%)"
},
"app.world.world-item.incompatible-version": {
"message": "Di-magkatugmang bersiyong {version}"
},
"app.world.world-item.not-played-yet": {
"message": "Hindi pa nalalaro"
"app.update.reload-to-update": {
"message": "Handang ma-install ang update"
},
"app.world.world-item.offline": {
"message": "Offline"
},
"app.world.world-item.players-online": {
"message": "{count} online"
},
"friends.action.add-friend": {
"message": "Magdagdag ng kaibigan"
},
@@ -515,12 +314,6 @@
"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"
},
@@ -587,12 +380,6 @@
"instance.settings.tabs.general.name": {
"message": "Pangalan"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.hooks": {
"message": "Mga launch hook"
},
@@ -728,24 +515,6 @@
"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"
},
@@ -769,26 +538,5 @@
},
"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"
}
}
+37 -385
View File
@@ -1,129 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Téléchargement de Java {version} en cours"
},
"app.action-bar.downloading-update": {
"message": "Téléchargement de la mise à jour"
},
"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.reload-to-update": {
"message": "Recharger pour mettre à jour"
},
"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.update": {
"message": "Mettre à jour"
},
"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 ou 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-servers-to-instance": {
"app.browse.add-server-to-instance": {
"message": "Ajouter le serveur à l'instance"
},
"app.browse.add-to-an-instance": {
"message": "Ajouter à une instance"
"app.browse.add-servers-to-instance": {
"message": "Ajouter des serveurs à votre instance"
},
"app.browse.add-to-instance": {
"message": "Ajouter à l'instance"
@@ -137,9 +23,6 @@
"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"
},
@@ -147,25 +30,13 @@
"message": "Découvrir des serveurs"
},
"app.browse.hide-added-servers": {
"message": "Masquer les serveurs déjà ajoutés"
"message": "Masquer les serveurs ajoutés"
},
"app.browse.project-type.modpacks": {
"message": "Modpacks"
"app.browse.hide-installed-content": {
"message": "Masquer le contenu installé"
},
"app.browse.server-instance-content-warning": {
"message": "Ajouter du contenu peut briser la comptabilité en rejoignant le serveur. Tout contenu en plus sera également perdu lorsque vous mettrez à jour le contenu de l'instance du serveur."
},
"app.browse.server.installing": {
"message": "Installation en cours"
},
"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.browse.install-content-to-instance": {
"message": "Installer du contenu à l'instance"
},
"app.export-modal.description-placeholder": {
"message": "Saisir la description du modpack..."
@@ -176,9 +47,6 @@
"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"
},
@@ -186,7 +54,7 @@
"message": "Nom du modpack"
},
"app.export-modal.select-files-label": {
"message": "Configurez quels fichiers sont inclus dans cette exportation"
"message": "Sélectionnez des fichiers et des dossiers à inclure dans le pack"
},
"app.export-modal.version-number-label": {
"message": "Numéro de version"
@@ -207,7 +75,7 @@
"message": "Supprimer l'instance"
},
"app.instance.modpack-already-installed.body": {
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr·e de vouloir le dupliquer ?"
"message": "Ce modpack est déjà installé sur l'instance <bold>{instanceName}</bold>. Êtes-vous sûr.e de vouloir le dupliquer ?"
},
"app.instance.modpack-already-installed.create": {
"message": "Créer"
@@ -231,7 +99,7 @@
"message": "Jette un coup d'œil aux projets que j'utilise dans mon modpack !"
},
"app.instance.mods.share-title": {
"message": "Le contenu de mon modpack"
"message": "Partagez le contenu de votre modpack"
},
"app.instance.mods.successfully-uploaded": {
"message": "Mis en ligne avec succès"
@@ -242,8 +110,11 @@
"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é"
@@ -263,8 +134,14 @@
"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..."
@@ -308,15 +185,6 @@
"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é."
},
@@ -341,152 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "Gestion des ressources"
},
"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.reorder-error.text": {
"message": "L'ordre des skins n'a pas pu être sauvegardé."
},
"app.skins.reorder-error.title": {
"message": "Impossible de réorganiser les skins"
},
"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": {
"message": "Modrinth App v{version} est prête à être installée ! Relancez l'application pour faire la mise à jour maintenant, ou automatiquement à la fermeture de Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} a finie d'être téléchargée. Rechargez pour mettre à jour maintenant, ou automatiquement quand vous fermez Modrinth App."
@@ -507,7 +231,7 @@
"message": "Téléchargement terminé"
},
"app.update-popup.reload": {
"message": "Recharger pour mettre à jour"
"message": "Recharger"
},
"app.update-popup.title": {
"message": "Mise à jour disponible"
@@ -518,6 +242,15 @@
"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"
},
@@ -533,6 +266,9 @@
"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"
},
@@ -704,30 +440,6 @@
"instance.settings.tabs.general.name": {
"message": "Nom"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canal de mise à jour"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Les versions stables, bêta et alpha seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Bêta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Les versions stables et bêta seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Stable"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Seules les versions stables seront affichées comme mises à jour disponibles."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Sélectionner le canal de mise à jour"
},
"instance.settings.tabs.hooks": {
"message": "Crochets de lancement"
},
@@ -776,24 +488,6 @@
"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"
},
@@ -809,9 +503,6 @@
"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"
},
@@ -884,24 +575,6 @@
"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"
},
@@ -925,26 +598,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Le loader est procuré par le serveur"
},
"unknown-pack-warning-modal.body": {
"message": "Un fichier nest révisé que sil 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"
}
}
+33 -27
View File
@@ -1,37 +1,16 @@
{
"app.action-bar.downloading-java": {
"message": "מוריד את Java {version}"
},
"app.action-bar.downloading-update": {
"message": "מוריד עדכון"
},
"app.action-bar.downloads": {
"message": "הורדות"
},
"app.action-bar.hide-more-running-instances": {
"message": "החבא יותר התקנים רצים"
},
"app.action-bar.make-primary-instance": {
"message": "הפוך למקרה ראשי"
},
"app.action-bar.offline": {
"message": "לא מקוון"
},
"app.action-bar.stop-instance": {
"message": ""
},
"app.appearance-settings.advanced-rendering.description": {
"message": "מאפשר עיבוד מתקדם כגון אפקטים טשטוש העלולים לגרום לבעיות ביצועים ללא עיבוד מואץ לחומרה."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "חזרה לעולמות"
},
"app.auth-servers.unreachable.body": {
"message": "ייתכן ששרתי האימות של Minecraft מושבתים כרגע. יש לבדוק את חיבור האינטרנט שלך ולנסות שוב מאוחר יותר."
},
"app.auth-servers.unreachable.header": {
"message": "לא ניתן לגשת לשרתי האימות"
},
"app.browse.add-server-to-instance": {
"message": "הוסף שרת להתקנה"
},
"app.browse.add-servers-to-instance": {
"message": "הוסף שרתים להתקנה שלך"
},
"app.browse.add-to-instance": {
"message": "הוספה להתקנה"
},
@@ -50,6 +29,15 @@
"app.browse.discover-servers": {
"message": "גלה שרתים"
},
"app.browse.hide-added-servers": {
"message": "הסתר שרתים שנוספו"
},
"app.browse.hide-installed-content": {
"message": "הסתר תוכן מותקן"
},
"app.browse.install-content-to-instance": {
"message": "הוספת התוכן להתקנה"
},
"app.export-modal.description-placeholder": {
"message": "הזן את תיאור חבילת המודים..."
},
@@ -65,6 +53,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "שם חבילת המודים"
},
"app.export-modal.select-files-label": {
"message": "בחר קבצים ותיקיות להכללה בחבילה הזו"
},
"app.export-modal.version-number-label": {
"message": "מספר גרסה"
},
@@ -218,6 +209,9 @@
"app.settings.tabs.resource-management": {
"message": "ניהול משאבים"
},
"app.update-popup.body": {
"message": "Modrinth App v{version} מוכנה להורדה!\nיש לרענן כדי לעדכן עכשיו, או באופן אוטומטי בעת סגירת האפליקציה."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} סיימה את תהליך ההורדה. יש לרענן כדי לעדכן עכשיו, או באופן אוטומטי בעת סגירת האפליקציה."
},
@@ -236,6 +230,9 @@
"app.update-popup.download-complete": {
"message": "הורדה הושלמה"
},
"app.update-popup.reload": {
"message": "רענן"
},
"app.update-popup.title": {
"message": "עדכון זמין"
},
@@ -245,6 +242,15 @@
"app.update.complete-toast.title": {
"message": "גרסה {version} הותקנה בהצלחה!"
},
"app.update.download-update": {
"message": "הורדת עדכון"
},
"app.update.downloading-update": {
"message": "מוריד עדכון ({percent}%)"
},
"app.update.reload-to-update": {
"message": "צריך לרענן כדי להתקין את העדכון"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
+32 -386
View File
@@ -1,132 +1,18 @@
{
"app.action-bar.downloading-java": {
"message": "A Java {version} letöltése"
},
"app.action-bar.downloading-update": {
"message": "Frissítés letöltése"
},
"app.action-bar.downloads": {
"message": "Letöltések"
},
"app.action-bar.hide-more-running-instances": {
"message": "További futó példányok elrejtése"
},
"app.action-bar.make-primary-instance": {
"message": "Beállítás elsődleges példányként"
},
"app.action-bar.no-instances-running": {
"message": "Nincsenek futó példányok"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Elsődleges példány"
},
"app.action-bar.reload-to-update": {
"message": "Töltsd újra a frissítéshez"
},
"app.action-bar.show-more-running-instances": {
"message": "További futó pédányok megjelenítése"
},
"app.action-bar.stop-instance": {
"message": "Példány leállítása"
},
"app.action-bar.update": {
"message": "Frissítés"
},
"app.action-bar.view-active-downloads": {
"message": "Aktív letöltések megtekintése"
},
"app.action-bar.view-instance": {
"message": "Példányok megtekintése"
},
"app.action-bar.view-logs": {
"message": "Naplók megtekintése"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Lehetővé teszi olyan speciális megjelenítési effektusok használatát, mint például az elmosódás, amelyek hardveres gyorsítás nélkül teljesítménycsökkenést okozhatnak."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Fejlett renderelés"
},
"app.appearance-settings.color-theme.description": {
"message": "Válaszd ki a Modrinth Apphoz a kívánt színsémát."
},
"app.appearance-settings.color-theme.title": {
"message": "Téma"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Állítsd be, hogy az indító melyik oldalon nyíljon meg."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Kezdőlap"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Könyvtár"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Alapértelmezett nyitóoldal"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Letiltja a játékosod felett megjelenő névcímkét a Kinézetek oldalon."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Névcímke eltüntetése"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "A kezdőlap „Folytasd itt” részében a legújabb világok is megtalálhatók."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Visszaugrás a világokba"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minimalizáld az indítót a Minecraft indításakor."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Indító minimalizálása"
},
"app.appearance-settings.native-decorations.description": {
"message": "A rendszer ablakkeret használata (az alkalmazás újraindítása szükséges)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Rendszer ablakkeret használata"
},
"app.appearance-settings.select-option": {
"message": "Válassz egy lehetőséget"
},
"app.appearance-settings.show-play-time.description": {
"message": "Megmutatja, mennyi időt töltöttél játszva egy profillal."
},
"app.appearance-settings.show-play-time.title": {
"message": "Játékidő megjelenítése"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Lehetővé teszi az oldalsáv be- és kikapcsolását."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Oldalsáv megjelenítése/elrejtése"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Ha olyan Modrinth Csomagfájlt (.mrpack) próbálsz telepíteni, amely nem a Modrinth szerverén található, a telepítés előtt gondoskodunk arról, hogy tisztában legyél a kockázatokkal."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Figyelmeztess, mielőtt ismeretlen modcsomagokat telepítenék"
},
"app.auth-servers.unreachable.body": {
"message": "A Minecraft hitelesítő szerverek lehet, hogy nem üzemelnek. Bizonyosodj meg róla, hogy van internetkapcsolatod és próbáld meg újra."
},
"app.auth-servers.unreachable.header": {
"message": "Nem lehet elérni a hitelesítési kiszolgálókat"
},
"app.browse.add-servers-to-instance": {
"message": "Szerver hozzáadása a példányhoz"
"app.browse.add-server-to-instance": {
"message": "Szerver hozzáadása a profilhoz"
},
"app.browse.add-to-an-instance": {
"message": "Hozzáadás egy példányhoz"
"app.browse.add-servers-to-instance": {
"message": "Szerverek hozzáadása a profilodhoz"
},
"app.browse.add-to-instance": {
"message": "Példányhoz adás"
"message": "Hozzáadás a profilhoz"
},
"app.browse.add-to-instance-name": {
"message": "Hozzáadás ehhez: {instanceName}"
@@ -137,9 +23,6 @@
"app.browse.already-added": {
"message": "Már hozzá van adva"
},
"app.browse.back-to-instance": {
"message": "Vissza a példányhoz"
},
"app.browse.discover-content": {
"message": "Tartalom böngészése"
},
@@ -147,25 +30,13 @@
"message": "Szerverek böngészése"
},
"app.browse.hide-added-servers": {
"message": "Már hozzáadott szerverek elrejtése"
"message": "Hozzáadott szerverek elrejtése"
},
"app.browse.project-type.modpacks": {
"message": "Modcsomagok"
"app.browse.hide-installed-content": {
"message": "A telepített tartalmak elrejtése"
},
"app.browse.server-instance-content-warning": {
"message": "A tartalom hozzáadása ronthatja a kompatibilitást a szerverhez való csatlakozáskor. A hozzáadott tartalom elveszik, ha frissíted a szerverprofil tartalmát."
},
"app.browse.server.installing": {
"message": "Letöltés..."
},
"app.content-install.no-compatible-versions": {
"message": "Nincs a(z) {compatibilityLabel} verziónak megfelelő elérhető verzió. Válassz ki egy verziót, ha mégis telepíteni szeretnéd. A függőségek nem települnek automatikusan."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modcsomag telepítése..."
"app.browse.install-content-to-instance": {
"message": "Tartalom letöltése a profilhoz"
},
"app.export-modal.description-placeholder": {
"message": "Írd be a modcsomag leírását..."
@@ -176,9 +47,6 @@
"app.export-modal.header": {
"message": "Modcsomag exportálása"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Tartalmaz „{file}” fájlokat?"
},
"app.export-modal.modpack-name-label": {
"message": "A modcsomag neve"
},
@@ -186,7 +54,7 @@
"message": "A modcsomag neve"
},
"app.export-modal.select-files-label": {
"message": "Állítsd be, hogy mely fájlok kerüljenek bele az exportba"
"message": "Válaszd ki a csomagba felveendő fájlokat és mappákat"
},
"app.export-modal.version-number-label": {
"message": "Verziószám"
@@ -216,7 +84,7 @@
"message": "A modcsomag már telepítve van"
},
"app.instance.modpack-already-installed.instance": {
"message": "Példány"
"message": "Profil"
},
"app.instance.mods.content-type-project": {
"message": "projekt"
@@ -315,16 +183,7 @@
"message": "Frissítés szükséges"
},
"app.modal.update-to-play.update-required-description": {
"message": "Frissítés szükséges ehhez: {name}. Kérlek, frissíts a legújabb verzióra a játék elindításához."
},
"app.project.install-button.already-installed": {
"message": "Ez a projekt már telepítve van"
},
"app.project.install-context.back-to-browse": {
"message": "Vissza a böngészéshez"
},
"app.project.install-context.install-content-to-instance": {
"message": "Tartalom telepítése a profilba"
"message": "\nFrissítés szükséges ehhez: {name}. Kérk, frissíts a legújabb verzióra a játék elindításához"
},
"app.settings.developer-mode-enabled": {
"message": "Fejlesztői mód bekapcsolva."
@@ -350,149 +209,11 @@
"app.settings.tabs.resource-management": {
"message": "Erőforráskezelés"
},
"app.skins.add-button": {
"message": "Kinézet hozzáadása"
},
"app.skins.add-button.drag-and-drop": {
"message": "Húzd ide a fájlt"
},
"app.skins.apply-button": {
"message": "Alkalmazás"
},
"app.skins.delete-button": {
"message": "Kinézet törlése"
},
"app.skins.delete-modal.description": {
"message": "Ezzel a kiválasztott kinézet véglegesen törlődik. Ez a művelet visszafordíthatatlan."
},
"app.skins.delete-modal.title": {
"message": "Biztos, hogy le akarod törölni ezt a kinézetet?"
},
"app.skins.dropped-file-error.text": {
"message": "Nem sikerült beolvasni a feltöltött fájlt."
},
"app.skins.dropped-file-error.title": {
"message": "Hiba a fájl feldolgozása közben"
},
"app.skins.edit-button": {
"message": "Kinézet szerkesztése"
},
"app.skins.modal.add-skin-button": {
"message": "Kinézet hozzáadása"
},
"app.skins.modal.add-title": {
"message": "Kinézet hozzáadása"
},
"app.skins.modal.arm-style-section": {
"message": "Karstílus"
},
"app.skins.modal.arm-style-slim": {
"message": "Vékony"
},
"app.skins.modal.arm-style-wide": {
"message": "Széles"
},
"app.skins.modal.cape-fallback-name": {
"message": "Köpeny"
},
"app.skins.modal.cape-section": {
"message": "Köpeny"
},
"app.skins.modal.edit-title": {
"message": "Kinézet szerkesztése"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Előbb módosíts valamit!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Nincs köpeny"
},
"app.skins.modal.none-cape-option": {
"message": "Nincs"
},
"app.skins.modal.replace-texture-button": {
"message": "Textúra cseréje"
},
"app.skins.modal.save-skin-button": {
"message": "Kinézet mentése"
},
"app.skins.modal.saving-tooltip": {
"message": "Mentés..."
},
"app.skins.modal.texture-section": {
"message": "Textúra"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Először tölts fel egy kinézetet!"
},
"app.skins.preview.edit-button": {
"message": "Kinézet szerkesztése"
},
"app.skins.previewing-badge": {
"message": "Előnézet"
},
"app.skins.rate-limit.text": {
"message": "Túl gyakran cseréled a kinézetedet. A Mojang szerverei ideiglenesen letiltották a további cseréket. Kérlek várj egy pillanatot, majd próbáld meg újra."
},
"app.skins.rate-limit.title": {
"message": "Lassíts!"
},
"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": "Alap kinézetek"
},
"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": "Ezeket a kinézeteket azért kaptad, mert a Pride-hónap során adományoztál a Modrinth Pride jótékonysági gyűjtéséhez."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Mentett kinézetek"
},
"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": "Bejelentkezés"
},
"app.skins.sign-in.description": {
"message": "Kérlek jelentkezz be a Minecraft-fiókodba, hogy használni tudd a Modrinth App kinézetkezelési funkcióit."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Izgatott Modrinth-bot"
},
"app.skins.sign-in.title": {
"message": "Kérlek jelentkezz be"
},
"app.skins.title": {
"message": "Kinézetváltó"
"app.update-popup.body": {
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez Töltsd újra az oldalt, vagy a Modrinth App bezárásakor automatikusan frissül."
},
"app.update-popup.body.download-complete": {
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás a bezárásakor automatikusan frissül."
"message": "A Modrinth App v{version} letöltése befejeződött. Frissítéshez Töltsd újra az oldalt, vagy a Modrinth App bezárásakor automatikusan frissül."
},
"app.update-popup.body.linux": {
"message": "A Modrinth App v{version} elérhető. Használd a csomagkezelőt a legújabb funkciók és javítások frissítéséhez!"
@@ -510,7 +231,7 @@
"message": "Sikeres letöltés"
},
"app.update-popup.reload": {
"message": "Töltsd újra a frissítéshez"
"message": "Újratöltés"
},
"app.update-popup.title": {
"message": "Frissítés elérhető"
@@ -521,6 +242,15 @@
"app.update.complete-toast.title": {
"message": "Verzió {version} sikeresen telepítve!"
},
"app.update.download-update": {
"message": "Frissítés letöltése"
},
"app.update.downloading-update": {
"message": "Frissítés letöltése ({percent}%)"
},
"app.update.reload-to-update": {
"message": "A telepítéshez újraindítás szükséges"
},
"app.world.server-modal.placeholder-address": {
"message": "pelda.modrinth.gg"
},
@@ -710,30 +440,6 @@
"instance.settings.tabs.general.name": {
"message": "Név"
},
"instance.settings.tabs.general.update-channel": {
"message": "Frissítési csatorna"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alfa"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "A kiadás, béta és az alfa verziók is meg fognak jelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Béta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "A kiadás és a béta verziók is meg fognak jelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Kiadás"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Csak a release verziók fognak megjelenni az elérhető frissítések között."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Válaszd ki a frissítési csatornát"
},
"instance.settings.tabs.hooks": {
"message": "Indítási horgok"
},
@@ -782,24 +488,6 @@
"instance.settings.tabs.java": {
"message": "Java és memória"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Egyéni környezeti változók"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Egyéni Java argumentumok"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Egyedi Java telepítés"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Egyéni memóriakiosztás"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Írja be a környezeti változókat..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Java argumentumok megadása..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Környezeti változók"
},
@@ -815,9 +503,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Hozzárendelt memória"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "Ablak"
},
@@ -861,7 +546,7 @@
"message": "Ne mutasd a kezdőlapon"
},
"instance.worlds.game_already_open": {
"message": "A példány már el van indítva"
"message": "A profil már meg van nyitva"
},
"instance.worlds.hardcore": {
"message": "Hardcore mód"
@@ -870,7 +555,7 @@
"message": "A szerver nem kompatibilis"
},
"instance.worlds.linked_server": {
"message": "A szerver projektje által kezelt"
"message": "Szerverprojekt által kezelt"
},
"instance.worlds.no_contact": {
"message": "A szerverrel nem lehet kapcsolatot létesíteni"
@@ -885,37 +570,19 @@
"message": "Játék a profillal"
},
"instance.worlds.view_instance": {
"message": "Példány megtekintése"
"message": "Profil megtekintése"
},
"instance.worlds.world_in_use": {
"message": "A világ használatban van"
},
"minecraft-account.add-account": {
"message": "Fiók hozzáadása"
},
"minecraft-account.label": {
"message": "Minecraft-fiók"
},
"minecraft-account.not-signed-in": {
"message": "Nem vagy bejelentkezve"
},
"minecraft-account.remove-account": {
"message": "Fiók törlése"
},
"minecraft-account.select-account": {
"message": "Fiók kiválasztása"
},
"minecraft-account.sign-in": {
"message": "Bejelentkezés a Minecraftba"
},
"search.filter.locked.instance": {
"message": "Pédány által megadva"
"message": "A profil által van megadva"
},
"search.filter.locked.instance-game-version.title": {
"message": "A játékverziót az adott példány biztosítja"
"message": "A játékverzió a profil által van megadva"
},
"search.filter.locked.instance-loader.title": {
"message": "A betöltőt az adott példány biztosítja"
"message": "A betöltő a profil által van megadva"
},
"search.filter.locked.instance.sync": {
"message": "Profil szinkronizálása"
@@ -931,26 +598,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "A betöltőt a szerver biztosítja"
},
"unknown-pack-warning-modal.body": {
"message": "Egy fájlt csak akkor vizsgálunk meg, ha azt feltöltik a Modrinthra, függetlenül a fájlformátumtól (beleértve a .mrpack formátumot is)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Ne mutasd újra ezt a figyelmeztetést"
},
"unknown-pack-warning-modal.header": {
"message": "Telepítés megerősítése"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Letöltés mindenképpen"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "A rosszindulatú programokat gyakran modcsomag-fájlokon keresztül terjesztik, például a Discordhoz hasonló platformokon történő megosztás révén."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Ezt a fájlt nem találtuk meg a Modrinthon. Határozottan javasoljuk, hogy kizárólag megbízható forrásokból származó fájlokat telepíts."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Figyelmeztetés ismeretlen fájlról"
}
}
+10 -481
View File
@@ -1,172 +1,10 @@
{
"app.action-bar.downloading-java": {
"message": "Mengunduh Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Mengunduh pembaruan"
},
"app.action-bar.downloads": {
"message": "Unduhan"
},
"app.action-bar.hide-more-running-instances": {
"message": "Sembunyikan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.make-primary-instance": {
"message": "Jadikan sebagai instans utama"
},
"app.action-bar.no-instances-running": {
"message": "Tidak ada instans yang sedang berjalan"
},
"app.action-bar.offline": {
"message": "Luring"
},
"app.action-bar.primary-instance": {
"message": "Instans utama"
},
"app.action-bar.reload-to-update": {
"message": "Muat ulang untuk memperbarui"
},
"app.action-bar.show-more-running-instances": {
"message": "Tampilkan lebih banyak instans yang sedang berjalan"
},
"app.action-bar.stop-instance": {
"message": "Hentikan instans"
},
"app.action-bar.update": {
"message": "Perbarui"
},
"app.action-bar.view-active-downloads": {
"message": "Lihat pengunduhan berlangsung"
},
"app.action-bar.view-instance": {
"message": "Lihat instans"
},
"app.action-bar.view-logs": {
"message": "Lihat catatan"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Menghidupkan renderasi tingkat lanjut seperti efek buram yang dapat menyebabkan masalah kinerja tanpa renderasi yang dipercepat perangkat keras."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Renderasi tingkat lanjut"
},
"app.appearance-settings.color-theme.description": {
"message": "Pilih tema warna pilihan Anda untuk Modrinth App."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema warna"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Ubah halaman utama yang dibuka oleh peluncur."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Beranda"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Pustaka"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Halaman arahan bawaan"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Mematikan label nama di atas wujud pemain Anda pada halaman rupa."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Sembunyikan label nama"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Menyertakan dunia terkini dalam bagian \"Masuk kembali\" pada Beranda."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Masuk kembali ke dunia"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Kecilkan pluncur ketika proses Minecraft berjalan."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Kecilkan peluncur"
},
"app.appearance-settings.native-decorations.description": {
"message": "Gunakan bingkai jendela sistem (wajib menjalankan ulang aplikasi)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Hiasan asli"
},
"app.appearance-settings.select-option": {
"message": "Pilih opsi"
},
"app.appearance-settings.show-play-time.description": {
"message": "Menampilkan seberapa lama Anda memainkan sebuah instans."
},
"app.appearance-settings.show-play-time.title": {
"message": "Tampilkan waktu bermain"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Menghidupkan kemampuan untuk menghidupkan dan mematikan bilah sisi."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Hidup/matikan bilah sisi"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Bila Anda mencoba memasangkan berkas Modrinth Pack (.mrpack) yang tidak dihoskan pada Modrinth, kami akan memastikan Anda untuk memahami risikonya sebelum memasangkannya."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Peringatkan saya sebelum memasang paket mod tidak dikenal"
},
"app.auth-servers.unreachable.body": {
"message": "Server autentikasi Minecraft mungkin sedang tidak tersedia saat ini. Periksa koneksi internet Anda dan coba lagi nanti."
},
"app.auth-servers.unreachable.header": {
"message": "Tidak dapat terhubung ke server autentikasi"
},
"app.browse.add-servers-to-instance": {
"message": "Menambah server ke instans"
},
"app.browse.add-to-an-instance": {
"message": "Tambah ke instans"
},
"app.browse.add-to-instance": {
"message": "Tambah ke instans"
},
"app.browse.add-to-instance-name": {
"message": "Tambah ke {instanceName}"
},
"app.browse.added": {
"message": "Ditambahkan"
},
"app.browse.already-added": {
"message": "Telah ditambahkan"
},
"app.browse.back-to-instance": {
"message": "Kembali ke instans"
},
"app.browse.discover-content": {
"message": "Temukan konten"
},
"app.browse.discover-servers": {
"message": "Temukan server"
},
"app.browse.hide-added-servers": {
"message": "Sembunyikan server yang sudah ditambah"
},
"app.browse.project-type.modpacks": {
"message": "Paket Mod"
},
"app.browse.server-instance-content-warning": {
"message": "Menambah konten dapat merusak kompatibilitas saat bergabung ke server. Konten tambahan apa pun akan hilang ketika Anda memperbarui konten instans server."
},
"app.browse.server.installing": {
"message": "Memasang"
},
"app.content-install.no-compatible-versions": {
"message": "Tidak ada versi tersedia yang cocok untuk {compatibilityLabel}. Anda tetap dapat memilih versi yang akan dipasang. Dependensi tidak akan dipasang secara otomatis."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Memasang paket mod..."
},
"app.export-modal.description-placeholder": {
"message": "Masukkan deskripsi paket mod..."
},
@@ -176,9 +14,6 @@
"app.export-modal.header": {
"message": "Ekspor paket mod"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Sertakan \"{file}\"?"
},
"app.export-modal.modpack-name-label": {
"message": "Nama Paket Mod"
},
@@ -186,7 +21,7 @@
"message": "Nama paket mod"
},
"app.export-modal.select-files-label": {
"message": "Konfigurasikan berkas mana yang disertakan dalam pengeksporan ini"
"message": "Pilih berkas dan folder yang ingin dimasukkan ke paket"
},
"app.export-modal.version-number-label": {
"message": "Nomor versi"
@@ -206,18 +41,9 @@
"app.instance.confirm-delete.header": {
"message": "Hapus instans"
},
"app.instance.modpack-already-installed.body": {
"message": "Paket mod ini telah terpasang di instans <bold>{instanceName}</bold>. Apakah Anda yakin ingin menggandakannya?"
},
"app.instance.modpack-already-installed.create": {
"message": "Buat"
},
"app.instance.modpack-already-installed.header": {
"message": "Paket mod telah terpasang"
},
"app.instance.modpack-already-installed.instance": {
"message": "Instans"
},
"app.instance.mods.content-type-project": {
"message": "proyek"
},
@@ -236,51 +62,6 @@
"app.instance.mods.successfully-uploaded": {
"message": "Berhasil diunggah"
},
"app.instance.worlds.add-server": {
"message": "Tambah server"
},
"app.instance.worlds.browse-servers": {
"message": "Telusuri server"
},
"app.instance.worlds.delete-world-description": {
"message": "'{name}' akan **dihapus secara permanen**, dan Anda tidak akan dapat memulihkannya."
},
"app.instance.worlds.delete-world-title": {
"message": "Apakah Anda yakin ingin menghapus dunia ini secara permanen?"
},
"app.instance.worlds.filter-modded": {
"message": "Dimodifikasi"
},
"app.instance.worlds.filter-offline": {
"message": "Luring"
},
"app.instance.worlds.filter-online": {
"message": "Daring"
},
"app.instance.worlds.filter-vanilla": {
"message": "Vanila"
},
"app.instance.worlds.no-worlds-description": {
"message": "Tambah server atau telusuri untuk memulai"
},
"app.instance.worlds.no-worlds-heading": {
"message": "Tidak ada server atau dunia yang ditambahkan"
},
"app.instance.worlds.remove-server-description": {
"message": "'{name}' akan dihapus dari daftar Anda, termasuk di dalam permainan, dan Anda tidak akan dapat memulihkannya."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "'{name}' ({address}) akan dihapus dari daftar Anda, termasuk di dalam permainan, dan Anda tidak akan dapat memulihkannya."
},
"app.instance.worlds.remove-server-title": {
"message": "Apakah Anda yakin ingin menghapus {name}?"
},
"app.instance.worlds.search-worlds-placeholder": {
"message": "Cari {count} dunia..."
},
"app.instance.worlds.this-server": {
"message": "server ini"
},
"app.modal.install-to-play.content-required": {
"message": "Konten diperlukan"
},
@@ -317,15 +98,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name} perlu diperbarui sebelum dimainkan. Mohon perbarui ke versi terkini untuk meluncurkan permainan."
},
"app.project.install-button.already-installed": {
"message": "Proyek ini sudah terpasang"
},
"app.project.install-context.back-to-browse": {
"message": "Kembali ke halaman pencarian"
},
"app.project.install-context.install-content-to-instance": {
"message": "Pasang konten ke instans"
},
"app.settings.developer-mode-enabled": {
"message": "Mode pengembang dihidupkan."
},
@@ -350,152 +122,8 @@
"app.settings.tabs.resource-management": {
"message": "Manajemen sumber"
},
"app.skins.add-button": {
"message": "Tambah rupa"
},
"app.skins.add-button.drag-and-drop": {
"message": "Seret dan lepas"
},
"app.skins.apply-button": {
"message": "Terapkan"
},
"app.skins.delete-button": {
"message": "Hapus rupa"
},
"app.skins.delete-modal.description": {
"message": "Ini akan menghapus rupa terpilih secara permanen. Tindakan ini tidak dapat dibatalkan."
},
"app.skins.delete-modal.title": {
"message": "Apakah Anda yakin ingin menghapus rupa ini?"
},
"app.skins.dropped-file-error.text": {
"message": "Gagal membaca berkas yang diletakkan."
},
"app.skins.dropped-file-error.title": {
"message": "Kesalahan dalam memroses berkas"
},
"app.skins.edit-button": {
"message": "Sunting rupa"
},
"app.skins.modal.add-skin-button": {
"message": "Tambah rupa"
},
"app.skins.modal.add-title": {
"message": "Menambah rupa"
},
"app.skins.modal.arm-style-section": {
"message": "Gaya lengan"
},
"app.skins.modal.arm-style-slim": {
"message": "Ramping"
},
"app.skins.modal.arm-style-wide": {
"message": "Lebar"
},
"app.skins.modal.cape-fallback-name": {
"message": "Jubah"
},
"app.skins.modal.cape-section": {
"message": "Jubah"
},
"app.skins.modal.edit-title": {
"message": "Menyunting rupa"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Sunting rupa terlebih dahulu!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Tanpa jubah"
},
"app.skins.modal.none-cape-option": {
"message": "Tidak ada"
},
"app.skins.modal.replace-texture-button": {
"message": "Ganti tekstur"
},
"app.skins.modal.save-skin-button": {
"message": "Simpan rupa"
},
"app.skins.modal.saving-tooltip": {
"message": "Menyimpan..."
},
"app.skins.modal.texture-section": {
"message": "Tekstur"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Unggah rupa terlebih dahulu!"
},
"app.skins.preview.edit-button": {
"message": "Sunting rupa"
},
"app.skins.previewing-badge": {
"message": "Mempratinjau"
},
"app.skins.rate-limit.text": {
"message": "Anda terlalu sering mengganti rupa Anda. Server Mojang telah memblokir permintaan lebih lanjut untuk sementara. Mohon tunggu sebentar sebelum mencoba lagi."
},
"app.skins.rate-limit.title": {
"message": "Pelan-pelan!"
},
"app.skins.reorder-error.text": {
"message": "Urutan rupa Anda tidak dapat disimpan."
},
"app.skins.reorder-error.title": {
"message": "Gagal mengurut ulang rupa"
},
"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": "Rupa bawaan"
},
"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": "Anda mendapat rupa ini karena telah berdonasi ke penggalangan dana Modrinth Pride saat Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Rupa tersimpan"
},
"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": "Masuk"
},
"app.skins.sign-in.description": {
"message": "Mohon masuk ke akun Minecraft Anda untuk menggunakan fitur pengelolaan rupa dari Modrinth App."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Bot Modrinth penuh antusias"
},
"app.skins.sign-in.title": {
"message": "Silakan masuk"
},
"app.skins.title": {
"message": "Pemilih rupa"
"app.update-popup.body": {
"message": "Modrinth App v{version} siap dipasang! Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} telah selesai mengunduh. Muat ulang untuk memperbarui sekarang, atau secara otomatis saat Anda menutup Modrinth App."
@@ -516,7 +144,7 @@
"message": "Selesai mengunduh"
},
"app.update-popup.reload": {
"message": "Muat ulang untuk memperbarui"
"message": "Muat ulang"
},
"app.update-popup.title": {
"message": "Pembaruan tersedia"
@@ -527,23 +155,14 @@
"app.update.complete-toast.title": {
"message": "Versi {version} berhasil dipasang!"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
"app.update.download-update": {
"message": "Unduh pembaruan"
},
"app.world.server-modal.select-an-option": {
"message": "Pilih opsi"
"app.update.downloading-update": {
"message": "Mengunduh pembaruan ({percent}%)"
},
"app.world.world-item.incompatible-version": {
"message": "Versi {version} tidak cocok"
},
"app.world.world-item.not-played-yet": {
"message": "Belum pernah dimainkan"
},
"app.world.world-item.offline": {
"message": "Luring"
},
"app.world.world-item.players-online": {
"message": "{count} orang daring"
"app.update.reload-to-update": {
"message": "Muat ulang untuk memasang pembaruan"
},
"friends.action.add-friend": {
"message": "Tambah teman"
@@ -644,12 +263,6 @@
"instance.edit-world.title": {
"message": "Sunting dunia"
},
"instance.files.adding-files": {
"message": "Menambahkan berkas ({completed}/{total})"
},
"instance.files.save-as": {
"message": "Simpan sebagai..."
},
"instance.server-modal.address": {
"message": "Alamat"
},
@@ -716,30 +329,6 @@
"instance.settings.tabs.general.name": {
"message": "Nama"
},
"instance.settings.tabs.general.update-channel": {
"message": "Perbarui kanal"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Versi rilisan, beta, dan alpha akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Versi beta dan alpha akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Rilisan"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Hanya versi rilisan yang akan ditampilkan sebagai pembaruan yang tersedia."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Pilih kanal pembaruan"
},
"instance.settings.tabs.hooks": {
"message": "Luncurkan kait"
},
@@ -788,24 +377,6 @@
"instance.settings.tabs.java": {
"message": "Java dan memori"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Variabel lingkungan tersuai"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Argumen Java tersuai"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Pemasangan Java tersuai"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Alokasi memori tersuai"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Masukkan variabel lingkungan..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Masukkan argumen Java..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variabel lingkungan"
},
@@ -821,9 +392,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memori yang teralokasikan"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/jalur/ke/java"
},
"instance.settings.tabs.window": {
"message": "Jendela"
},
@@ -896,24 +464,6 @@
"instance.worlds.world_in_use": {
"message": "Dunia sedang dimainkan"
},
"minecraft-account.add-account": {
"message": "Tambah akun"
},
"minecraft-account.label": {
"message": "Akun Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Tidak masuk"
},
"minecraft-account.remove-account": {
"message": "Hapus akun"
},
"minecraft-account.select-account": {
"message": "Pilih akun"
},
"minecraft-account.sign-in": {
"message": "Masuk ke Minecraft"
},
"search.filter.locked.instance": {
"message": "Disediakan oleh instans"
},
@@ -937,26 +487,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Pemuat disediakan oleh server"
},
"unknown-pack-warning-modal.body": {
"message": "Berkas hanya akan ditinjau bila ia diunggah ke Modrinth, tak peduli format berkasnya (termasuk .mrpack)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Jangan tampilkan peringatan ini lagi"
},
"unknown-pack-warning-modal.header": {
"message": "Konfirmasi pemasangan"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Tetap pasang"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Program jahat (malware) dibagikan melalui berkas paket mod dengan membagikannya melalui platform seperti Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Kami tidak dapat menemukan berkas ini di Modrinth. Kami sangat menyarankan Anda untuk hanya memasang berkas dari sumber-sumber terpercaya."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Peringatan berkas tidak dikenal"
}
}
+37 -385
View File
@@ -1,129 +1,15 @@
{
"app.action-bar.downloading-java": {
"message": "Java {version}"
},
"app.action-bar.downloading-update": {
"message": "Scaricando l'aggiornamento"
},
"app.action-bar.downloads": {
"message": "Download"
},
"app.action-bar.hide-more-running-instances": {
"message": "Mostra meno istanze in esecuzione"
},
"app.action-bar.make-primary-instance": {
"message": "Rendi primaria"
},
"app.action-bar.no-instances-running": {
"message": "Nessuna istanza in esecuzione"
},
"app.action-bar.offline": {
"message": "Offline"
},
"app.action-bar.primary-instance": {
"message": "Primaria"
},
"app.action-bar.reload-to-update": {
"message": "Ricarica per aggiornare"
},
"app.action-bar.show-more-running-instances": {
"message": "Mostra più istanze in esecuzione"
},
"app.action-bar.stop-instance": {
"message": "Ferma l'istanza"
},
"app.action-bar.update": {
"message": "Aggiorna"
},
"app.action-bar.view-active-downloads": {
"message": "Mostra i download attivi"
},
"app.action-bar.view-instance": {
"message": "Mostra l'istanza"
},
"app.action-bar.view-logs": {
"message": "Mostra i log"
},
"app.appearance-settings.advanced-rendering.description": {
"message": "Abilita alcuni effetti come la sfocatura, ma può causare problemi di prestazioni senza accelerazione hardware."
},
"app.appearance-settings.advanced-rendering.title": {
"message": "Rendering avanzato"
},
"app.appearance-settings.color-theme.description": {
"message": "Scegli il tema che Modrinth App userà su questo dispositivo."
},
"app.appearance-settings.color-theme.title": {
"message": "Tema"
},
"app.appearance-settings.default-landing-page.description": {
"message": "Imposta la pagina che si aprirà all'avvio del launcher."
},
"app.appearance-settings.default-landing-page.home": {
"message": "Home"
},
"app.appearance-settings.default-landing-page.library": {
"message": "Libreria"
},
"app.appearance-settings.default-landing-page.title": {
"message": "Pagina di avvio"
},
"app.appearance-settings.hide-nametag.description": {
"message": "Rimuove il nametag sopra la testa del giocatore nella pagina delle skin."
},
"app.appearance-settings.hide-nametag.title": {
"message": "Nascondi il nome"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "Elenca i mondi recenti nella sezione \"Avvio rapido\" della pagina Home."
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "Avvio rapido nei mondi"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Riduci il launcher a icona quando si avvia Minecraft."
},
"app.appearance-settings.minimize-launcher.title": {
"message": "Riduci a icona"
},
"app.appearance-settings.native-decorations.description": {
"message": "Usa la cornice di sistema (riavvio necessario)."
},
"app.appearance-settings.native-decorations.title": {
"message": "Decorazioni native"
},
"app.appearance-settings.select-option": {
"message": "Seleziona"
},
"app.appearance-settings.show-play-time.description": {
"message": "Mostra quanto tempo hai speso giocando a un'istanza."
},
"app.appearance-settings.show-play-time.title": {
"message": "Mostra le ore di gioco"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "Mostra o nascondi la barra laterale."
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "Abilita la barra laterale"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Mostrerà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, a causa dei possibili rischi."
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "Avvisami se installo pacchetti sconosciuti"
},
"app.auth-servers.unreachable.body": {
"message": "I server di autenticazione di Minecraft stanno riscontrando problemi. Controlla la tua connessione a Internet e riprova più tardi."
},
"app.auth-servers.unreachable.header": {
"message": "Impossibile raggiungere i server di autenticazione"
},
"app.browse.add-servers-to-instance": {
"message": "Aggiungendo il server all'istanza"
"app.browse.add-server-to-instance": {
"message": "Aggiungi server all'istanza"
},
"app.browse.add-to-an-instance": {
"message": "Aggiungi a un'istanza"
"app.browse.add-servers-to-instance": {
"message": "Aggiungi i server alla tua istanza"
},
"app.browse.add-to-instance": {
"message": "Aggiungi all'istanza"
@@ -137,9 +23,6 @@
"app.browse.already-added": {
"message": "Già aggiunto"
},
"app.browse.back-to-instance": {
"message": "Torna all'istanza"
},
"app.browse.discover-content": {
"message": "Esplora i contenuti"
},
@@ -147,25 +30,13 @@
"message": "Esplora i server"
},
"app.browse.hide-added-servers": {
"message": "Nascondi server già aggiunti"
"message": "Nascondi server aggiunti"
},
"app.browse.project-type.modpacks": {
"message": "Pacchetti di mod"
"app.browse.hide-installed-content": {
"message": "Nascondi contenuti installati"
},
"app.browse.server-instance-content-warning": {
"message": "Aggiungere dei contenuti potrebbe causare problemi entrando nel server. Inoltre, qualsiasi contenuto aggiunto sarà perso quando aggiorni i contenuti dell'istanza."
},
"app.browse.server.installing": {
"message": "Installazione"
},
"app.content-install.no-compatible-versions": {
"message": "Nessuna versione disponibile soddisfa {compatibilityLabel}. Puoi selezionare una versione da installare comunque. Le dipendenze non verranno scaricate automaticamente."
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Installando il pacchetto..."
"app.browse.install-content-to-instance": {
"message": "Installa contenuti nell'istanza"
},
"app.export-modal.description-placeholder": {
"message": "Inserisci descrizione del pacchetto..."
@@ -176,9 +47,6 @@
"app.export-modal.header": {
"message": "Esporta pacchetto"
},
"app.export-modal.include-file-accessibility-label": {
"message": "Includere \"{file}\" nell'esportazione?"
},
"app.export-modal.modpack-name-label": {
"message": "Nome del pacchetto"
},
@@ -186,7 +54,7 @@
"message": "Nome del pacchetto"
},
"app.export-modal.select-files-label": {
"message": "Seleziona i file da includere in questa esportazione"
"message": "Seleziona i file e le cartelle da includere nel pacchetto"
},
"app.export-modal.version-number-label": {
"message": "Numero di versione"
@@ -224,6 +92,9 @@
"app.instance.mods.project-was-added": {
"message": "\"{name}\" è stato aggiunto"
},
"app.instance.mods.projects-were-added": {
"message": "{count} progetti aggiunti"
},
"app.instance.mods.share-text": {
"message": "Guarda le mod che uso nel mio pacchetto!"
},
@@ -239,6 +110,9 @@
"app.instance.worlds.browse-servers": {
"message": "Esplora i server"
},
"app.instance.worlds.delete-world-description": {
"message": "\"{name}\" verrà **eliminato permanentemente** e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.delete-world-title": {
"message": "Vuoi davvero eliminare questo mondo per sempre?"
},
@@ -260,6 +134,12 @@
"app.instance.worlds.no-worlds-heading": {
"message": "Nessun server o mondo aggiunto"
},
"app.instance.worlds.remove-server-description": {
"message": "\"{name}\" sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.remove-server-description-with-address": {
"message": "\"{name}\" ({address}) sarà rimosso dalla tua lista, inclusa quella nel gioco, e non ci sarà modo di recuperarlo."
},
"app.instance.worlds.remove-server-title": {
"message": "Vuoi davvero rimuovere {name}?"
},
@@ -305,15 +185,6 @@
"app.modal.update-to-play.update-required-description": {
"message": "{name} richiede degli aggiornamenti. Installa l'ultima versione per poter giocare."
},
"app.project.install-button.already-installed": {
"message": "Questo progetto è già stato installato"
},
"app.project.install-context.back-to-browse": {
"message": "Torna a Esplora"
},
"app.project.install-context.install-content-to-instance": {
"message": "Installa il contenuto nell'istanza"
},
"app.settings.developer-mode-enabled": {
"message": "Modalità sviluppatore attiva."
},
@@ -338,152 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "Gestione delle risorse"
},
"app.skins.add-button": {
"message": "Nuova skin"
},
"app.skins.add-button.drag-and-drop": {
"message": "Trascina qui"
},
"app.skins.apply-button": {
"message": "Applica"
},
"app.skins.delete-button": {
"message": "Elimina"
},
"app.skins.delete-modal.description": {
"message": "Questa skin sarà rimossa per sempre. Quest'azione non può essere annullata."
},
"app.skins.delete-modal.title": {
"message": "Vuoi davvero eliminare questa skin?"
},
"app.skins.dropped-file-error.text": {
"message": "Non è stato possibile leggere il file trascinato."
},
"app.skins.dropped-file-error.title": {
"message": "Impossibile processare il file"
},
"app.skins.edit-button": {
"message": "Modifica"
},
"app.skins.modal.add-skin-button": {
"message": "Aggiungi"
},
"app.skins.modal.add-title": {
"message": "Aggiungi una skin"
},
"app.skins.modal.arm-style-section": {
"message": "Modello"
},
"app.skins.modal.arm-style-slim": {
"message": "Snello"
},
"app.skins.modal.arm-style-wide": {
"message": "Largo"
},
"app.skins.modal.cape-fallback-name": {
"message": "Mantello"
},
"app.skins.modal.cape-section": {
"message": "Mantello"
},
"app.skins.modal.edit-title": {
"message": "Modifica la skin"
},
"app.skins.modal.make-edit-first-tooltip": {
"message": "Modifica prima la skin!"
},
"app.skins.modal.no-cape-tooltip": {
"message": "Nessun mantello"
},
"app.skins.modal.none-cape-option": {
"message": "Nessuno"
},
"app.skins.modal.replace-texture-button": {
"message": "Cambia"
},
"app.skins.modal.save-skin-button": {
"message": "Salva"
},
"app.skins.modal.saving-tooltip": {
"message": "Salvando..."
},
"app.skins.modal.texture-section": {
"message": "Aspetto"
},
"app.skins.modal.upload-skin-first-tooltip": {
"message": "Carica prima una skin!"
},
"app.skins.preview.edit-button": {
"message": "Modifica"
},
"app.skins.previewing-badge": {
"message": "Anteprima"
},
"app.skins.rate-limit.text": {
"message": "Stai cambiando skin troppo spesso. I server di Mojang hanno limitato le nostre richieste. Aspetta un momento prima di riprovare."
},
"app.skins.rate-limit.title": {
"message": "Rallenta!"
},
"app.skins.reorder-error.text": {
"message": "L'ordine delle skin non è stato salvato."
},
"app.skins.reorder-error.title": {
"message": "Errore nel riordine delle skin"
},
"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": "Skin predefinite"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Pride Modrinth"
},
"app.skins.section.modrinth-pride.tooltip": {
"message": "Hai ottenuto queste skin per aver donato alla raccolta fondi per il Pride durante il Pride Month."
},
"app.skins.section.mounts-of-mayhem": {
"message": "Mounts of Mayhem"
},
"app.skins.section.saved-skins": {
"message": "Skin salvate"
},
"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": "Accedi"
},
"app.skins.sign-in.description": {
"message": "Accedi col tuo account Minecraft per poter gestire le skin qui."
},
"app.skins.sign-in.rinthbot-alt": {
"message": "Modrinth Bot emozionato"
},
"app.skins.sign-in.title": {
"message": "Effettua l'accesso"
},
"app.skins.title": {
"message": "Seleziona una skin"
"app.update-popup.body": {
"message": "Modrinth App v{version} è pronta per essere installata! Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} è stata scaricata. Ricarica per aggiornare ora, o avverrà in automatico alla chiusura dell'app."
@@ -515,6 +242,15 @@
"app.update.complete-toast.title": {
"message": "La versione {version} è stata installata con successo!"
},
"app.update.download-update": {
"message": "Scarica aggiornamento"
},
"app.update.downloading-update": {
"message": "Scaricando l'aggiornamento ({percent}%)"
},
"app.update.reload-to-update": {
"message": "Ricarica per installare l'aggiornamento"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -537,7 +273,7 @@
"message": "Stringi un'amicizia"
},
"friends.action.view-friend-requests": {
"message": "{count, plural, one {# richiesta} other {# richieste}} d''amicizia"
"message": "{count} {count, plural, one {richiesta} other {richieste}} d''amicizia"
},
"friends.add-friend.submit": {
"message": "Invia richiesta d'amicizia"
@@ -588,7 +324,7 @@
"message": "Nessuna amicizia corrisponde a ''{query}''"
},
"friends.search-friends-placeholder": {
"message": "Cerca tra le amicizie..."
"message": "Cerca amicizie..."
},
"friends.section.heading": {
"message": "{title} - {count}"
@@ -627,7 +363,7 @@
"message": "Mondo di Minecraft"
},
"instance.edit-world.reset-icon": {
"message": "Ripristina icona"
"message": "Resetta icona"
},
"instance.edit-world.title": {
"message": "Modifica mondo"
@@ -687,7 +423,7 @@
"message": "Cambia icona"
},
"instance.settings.tabs.general.edit-icon.select": {
"message": "Scegli un'icona"
"message": "Seleziona icona"
},
"instance.settings.tabs.general.library-groups": {
"message": "Gruppi della libreria"
@@ -704,30 +440,6 @@
"instance.settings.tabs.general.name": {
"message": "Nome"
},
"instance.settings.tabs.general.update-channel": {
"message": "Canale di aggiornamento"
},
"instance.settings.tabs.general.update-channel.alpha": {
"message": "Alpha"
},
"instance.settings.tabs.general.update-channel.alpha.description": {
"message": "Le versioni stabili, beta e alpha saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.beta": {
"message": "Beta"
},
"instance.settings.tabs.general.update-channel.beta.description": {
"message": "Le versioni stabili e beta saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.release": {
"message": "Stabile"
},
"instance.settings.tabs.general.update-channel.release.description": {
"message": "Solo le versioni stabili saranno mostrate come aggiornamenti disponibili."
},
"instance.settings.tabs.general.update-channel.select": {
"message": "Seleziona il canale di aggiornamento"
},
"instance.settings.tabs.hooks": {
"message": "Hook di avvio"
},
@@ -776,24 +488,6 @@
"instance.settings.tabs.java": {
"message": "Java e memoria"
},
"instance.settings.tabs.java.custom-environment-variables": {
"message": "Personalizzate"
},
"instance.settings.tabs.java.custom-java-arguments": {
"message": "Personalizzati"
},
"instance.settings.tabs.java.custom-java-installation": {
"message": "Personalizzata"
},
"instance.settings.tabs.java.custom-memory-allocation": {
"message": "Personalizzata"
},
"instance.settings.tabs.java.enter-environment-variables": {
"message": "Inserisci le variabili d'ambiente..."
},
"instance.settings.tabs.java.enter-java-arguments": {
"message": "Inserisci gli argomenti JVM..."
},
"instance.settings.tabs.java.environment-variables": {
"message": "Variabili d'ambiente"
},
@@ -809,9 +503,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "Memoria allocata"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/percorso/di/java"
},
"instance.settings.tabs.window": {
"message": "Finestra"
},
@@ -884,24 +575,6 @@
"instance.worlds.world_in_use": {
"message": "Mondo già in uso"
},
"minecraft-account.add-account": {
"message": "Aggiungi account"
},
"minecraft-account.label": {
"message": "Account di Minecraft"
},
"minecraft-account.not-signed-in": {
"message": "Accesso non effettuato"
},
"minecraft-account.remove-account": {
"message": "Rimuovi account"
},
"minecraft-account.select-account": {
"message": "Seleziona un account"
},
"minecraft-account.sign-in": {
"message": "Accedi a Minecraft"
},
"search.filter.locked.instance": {
"message": "Determinato dall'istanza"
},
@@ -925,26 +598,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "Il loader è determinato dal server"
},
"unknown-pack-warning-modal.body": {
"message": "Solo i file caricati su Modrinth vengono esaminati, qualunque sia il loro formato (.mrpack inclusi)."
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "Non mostrare più questo avviso"
},
"unknown-pack-warning-modal.header": {
"message": "Conferma l'installazione"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "Installa comunque"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "Spesso i malware vengono nascosti nei pacchetti di mod, poi distribuiti su piattaforme come Discord."
},
"unknown-pack-warning-modal.warning.body": {
"message": "Non è stato possibile trovare questo file su Modrinth. Consigliamo di installare file solo da fonti attendibili."
},
"unknown-pack-warning-modal.warning.title": {
"message": "Tipo di file sconosciuto"
}
}
+28 -220
View File
@@ -1,118 +1,16 @@
{
"app.action-bar.downloading-java": {
"message": "Java {version} をダウンロード中"
},
"app.action-bar.downloading-update": {
"message": "アップデートをダウンロード中"
},
"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.advanced-rendering.description": {
"message": "ぼかし効果などの高度なレンダリングを有効にします。グラフィックアクセラレーションが使用できない場合は、パフォーマンスが低下する可能性があります。"
},
"app.appearance-settings.advanced-rendering.title": {
"message": "高度なレンダリング"
},
"app.appearance-settings.color-theme.description": {
"message": "Modrinth Appでのお好みのテーマを選択してください。"
},
"app.appearance-settings.color-theme.title": {
"message": "テーマ"
},
"app.appearance-settings.default-landing-page.description": {
"message": "ランチャー起動時に表示するページを変更します"
},
"app.appearance-settings.default-landing-page.home": {
"message": "ホーム"
},
"app.appearance-settings.default-landing-page.library": {
"message": "ライブラリ"
},
"app.appearance-settings.default-landing-page.title": {
"message": "デフォルトの起動ページ"
},
"app.appearance-settings.hide-nametag.description": {
"message": "スキンページでプレイヤー上部のネームタグを非表示にします。"
},
"app.appearance-settings.hide-nametag.title": {
"message": "ネームタグを非表示にする"
},
"app.appearance-settings.jump-back-into-worlds.description": {
"message": "ホームページの「ジャンプバック」セクションには、最近追加されたワールドが含まれています。"
},
"app.appearance-settings.jump-back-into-worlds.title": {
"message": "最近のワールドを表示"
},
"app.appearance-settings.minimize-launcher.description": {
"message": "Minecraftが起動した時、ランチャーを最小化します。"
},
"app.appearance-settings.minimize-launcher.title": {
"message": "ランチャーを最小化"
},
"app.appearance-settings.native-decorations.description": {
"message": "OSデフォルトのウィンドウUIを使用します。(アプリの再起動が必要です)"
},
"app.appearance-settings.native-decorations.title": {
"message": "OSのウィンドウUIを使用"
},
"app.appearance-settings.select-option": {
"message": "オプションを選択してください"
},
"app.appearance-settings.show-play-time.description": {
"message": "インスタンスのプレイ時間を表示します。"
},
"app.appearance-settings.show-play-time.title": {
"message": "プレイ時間を表示"
},
"app.appearance-settings.toggle-sidebar.description": {
"message": "サイドバーの表示/非表示を切り替える機能を有効にします。"
},
"app.appearance-settings.toggle-sidebar.title": {
"message": "サイドバーの表示切替を有効化"
},
"app.appearance-settings.unknown-pack-warning.description": {
"message": "Modrinthで配布されていないModrinth Pack(.mrpack)をインストールする場合、事前にリスクについて警告します。"
},
"app.appearance-settings.unknown-pack-warning.title": {
"message": "不明なModパックのインストール前に警告する"
},
"app.auth-servers.unreachable.body": {
"message": "Minecraftの認証サーバーは現在停止している可能性があります。インターネット接続を確認し、しばらくしてからもう一度お試しください。"
},
"app.auth-servers.unreachable.header": {
"message": "認証サーバーにアクセスできません"
},
"app.browse.add-server-to-instance": {
"message": "サーバーをインスタンスに追加"
},
"app.browse.add-servers-to-instance": {
"message": "サーバーを自身のインスタンスに追加"
},
"app.browse.add-to-instance": {
"message": "インスタンスに追加"
},
@@ -132,19 +30,13 @@
"message": "サーバーを探す"
},
"app.browse.hide-added-servers": {
"message": "既に追加済みのサーバーを非表示にする"
"message": "追加済みのサーバーを隠す"
},
"app.browse.project-type.modpacks": {
"message": "Modパック"
"app.browse.hide-installed-content": {
"message": "インストール済みのコンテンツを隠す"
},
"app.browse.server.installing": {
"message": "インストール"
},
"app.creation-modal.installing-modpack.description": {
"message": "{fileName}"
},
"app.creation-modal.installing-modpack.title": {
"message": "Modパックをインストール中..."
"app.browse.install-content-to-instance": {
"message": "コンテンツをインスタンスにインストールする"
},
"app.export-modal.description-placeholder": {
"message": "Modパックの説明を入力…"
@@ -161,6 +53,9 @@
"app.export-modal.modpack-name-placeholder": {
"message": "Modパック名"
},
"app.export-modal.select-files-label": {
"message": "パックに含めるファイルとフォルダーを選択"
},
"app.export-modal.version-number-label": {
"message": "バージョン番号"
},
@@ -314,65 +209,8 @@
"app.settings.tabs.resource-management": {
"message": "リソース管理"
},
"app.skins.modal.arm-style-slim": {
"message": "スリム"
},
"app.skins.modal.arm-style-wide": {
"message": "ワイド"
},
"app.skins.modal.cape-fallback-name": {
"message": "マント"
},
"app.skins.modal.cape-section": {
"message": "マント"
},
"app.skins.modal.no-cape-tooltip": {
"message": "マントなし"
},
"app.skins.modal.replace-texture-button": {
"message": "置きかえる"
},
"app.skins.modal.texture-section": {
"message": "テクスチャ"
},
"app.skins.preview.edit-button": {
"message": "編集"
},
"app.skins.section.builders-and-biomes": {
"message": "ビルダーズ&バイオーム"
},
"app.skins.section.chase-the-skies": {
"message": "チェイス・ザ・スカイ"
},
"app.skins.section.minecon-earth-2017": {
"message": "MINECON Earth 2017"
},
"app.skins.section.modrinth": {
"message": "Modrinth"
},
"app.skins.section.modrinth-pride": {
"message": "Modrinth プライド"
},
"app.skins.section.mounts-of-mayhem": {
"message": "マウント・オブ・メイヘム"
},
"app.skins.section.saved-skins": {
"message": "保存済みのスキン"
},
"app.skins.section.striding-hero": {
"message": "Striding Hero"
},
"app.skins.section.the-copper-age": {
"message": "銅の時代"
},
"app.skins.section.the-garden-awakens": {
"message": "ガーデン・アウェイケンズ"
},
"app.skins.section.tiny-takeover": {
"message": "タイニー・テイクオーバー"
},
"app.skins.sign-in.rinthbot-alt": {
"message": "ワクワクしている Modrinth ボット"
"app.update-popup.body": {
"message": "Modrinth App v{version} のインストール準備が整いました!今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
},
"app.update-popup.body.download-complete": {
"message": "Modrinth App v{version} のダウンロードが完了しました。今すぐ更新するには再読み込みするか、Modrinth Appを閉じる際に自動更新されます。"
@@ -392,6 +230,9 @@
"app.update-popup.download-complete": {
"message": "ダウンロード完了"
},
"app.update-popup.reload": {
"message": "再読み込み"
},
"app.update-popup.title": {
"message": "アップデートが利用可能です"
},
@@ -401,6 +242,15 @@
"app.update.complete-toast.title": {
"message": "バージョン {version} が正常にインストールされました!"
},
"app.update.download-update": {
"message": "アップデートをダウンロード"
},
"app.update.downloading-update": {
"message": "アップデートをダウンロード中 ({percent}%)"
},
"app.update.reload-to-update": {
"message": "再起動して今すぐ更新"
},
"app.world.server-modal.placeholder-address": {
"message": "example.modrinth.gg"
},
@@ -653,9 +503,6 @@
"instance.settings.tabs.java.java-memory": {
"message": "メモリ割り当て"
},
"instance.settings.tabs.java.java-path-placeholder": {
"message": "/path/to/java"
},
"instance.settings.tabs.window": {
"message": "ウィンドウ"
},
@@ -728,24 +575,6 @@
"instance.worlds.world_in_use": {
"message": "ワールドは使用中"
},
"minecraft-account.add-account": {
"message": "アカウントを追加"
},
"minecraft-account.label": {
"message": "Minecraftアカウント"
},
"minecraft-account.not-signed-in": {
"message": "サインインしていません"
},
"minecraft-account.remove-account": {
"message": "アカウントを一覧から削除"
},
"minecraft-account.select-account": {
"message": "アカウントを選択"
},
"minecraft-account.sign-in": {
"message": "Minecraftにサインイン"
},
"search.filter.locked.instance": {
"message": "インスタンスによる条件"
},
@@ -769,26 +598,5 @@
},
"search.filter.locked.server-loader.title": {
"message": "ローダーはサーバーによる条件です"
},
"unknown-pack-warning-modal.body": {
"message": "ファイル形式に関わらず、Modrinthにアップロードされたファイルのみが確認されます。(.mrpackを含む)"
},
"unknown-pack-warning-modal.dont-show-again": {
"message": "この警告を次回から表示しない"
},
"unknown-pack-warning-modal.header": {
"message": "インストールの確認"
},
"unknown-pack-warning-modal.install-anyway": {
"message": "インストールを続行"
},
"unknown-pack-warning-modal.malware-statement": {
"message": "一般的にマルウェアは、Discord等のプラットフォーム上でModパックファイルを配布して拡散されます"
},
"unknown-pack-warning-modal.warning.body": {
"message": "このファイルをModrinth上で見つけることができませんでした。信頼できるソースからインストールすることを強くお勧めします。"
},
"unknown-pack-warning-modal.warning.title": {
"message": "不明なファイルの警告"
}
}

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