mirror of
https://github.com/modrinth/code.git
synced 2026-08-02 14:15:53 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d1ddeda24 | ||
|
|
a0228dc2b1 | ||
|
|
07f9e3aedc | ||
|
|
a79b8e0777 | ||
|
|
671f6d264a | ||
|
|
e231df1f97 | ||
|
|
3052a14d95 | ||
|
|
e8665f43ca | ||
|
|
cba4550be4 | ||
|
|
384556a810 | ||
|
|
a082e8597c | ||
|
|
7048a35e9f | ||
|
|
c166ce52b3 | ||
|
|
9c99518497 | ||
|
|
758ed818c8 | ||
|
|
83e45d7a5c | ||
|
|
77b30b27fe | ||
|
|
3d7aea5a45 | ||
|
|
fd5d2797b3 | ||
|
|
ec85d9de1c | ||
|
|
22415a4cc6 | ||
|
|
3c6fadf5e8 | ||
|
|
871672d8bf | ||
|
|
e8dc3c3150 | ||
|
|
56dae8f104 | ||
|
|
8af43e59b9 | ||
|
|
ae9ca4db18 | ||
|
|
4eeb53c429 | ||
|
|
c69f24f94d | ||
|
|
de07bcff7d | ||
|
|
beff44767e | ||
|
|
5875e4332f | ||
|
|
f9c078d29d | ||
|
|
418e4f281d | ||
|
|
99ac6b87b1 | ||
|
|
159c6205ef | ||
|
|
bb4862daa6 | ||
|
|
6ed56a4756 | ||
|
|
93b21bb107 | ||
|
|
abc6cf59d8 | ||
|
|
849fe4e1ba | ||
|
|
b442fa4cca | ||
|
|
118046d690 | ||
|
|
b6bca2aaeb | ||
|
|
dcab665455 | ||
|
|
2f311643a0 | ||
|
|
e13a89dd72 | ||
|
|
565ac2cb53 | ||
|
|
7d6f77bebf | ||
|
|
b53887997c | ||
|
|
9a499af03e | ||
|
|
f6edc3ab58 | ||
|
|
c1d7aa494c | ||
|
|
a4c8154438 | ||
|
|
7dbbbe590f | ||
|
|
8a72ee9968 | ||
|
|
2da2035a6f |
@@ -0,0 +1,171 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2024 CARIAD SE
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
name: 'Merge Queue CI Check Skipper'
|
||||
description: 'Outputs `skip-check` as `true` if this is running as part of merge queue checks and the same checks have already been executed in the PR itself.'
|
||||
inputs:
|
||||
secret:
|
||||
description: 'Optional GitHub Secret that can access branch protection rules using the administration:read permission'
|
||||
required: false
|
||||
outputs:
|
||||
skip-check:
|
||||
description: 'Skip Check (boolean)'
|
||||
value: ${{ steps.passed-checks.outputs.can-skip-checks }}
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract PR Number and Commit ID
|
||||
id: extract-pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const githubRef = process.env.GITHUB_REF;
|
||||
const regex = /^refs\/heads\/gh-readonly-queue\/([a-zA-Z0-9.\-_\/]+)\/pr-(\d+)-([a-f0-9]+)$/;
|
||||
|
||||
if (regex.test(githubRef)) {
|
||||
const [, targetBranchName, prNumber, commitId] = githubRef.match(regex);
|
||||
core.setOutput('targetBranchName', targetBranchName);
|
||||
core.setOutput('prNumber', prNumber);
|
||||
core.setOutput('commitId', commitId);
|
||||
} else {
|
||||
console.log(`GITHUB_REF is not a merge queue ref, setting CAN_SKIP_CHECKS to false: ${githubRef}`);
|
||||
core.exportVariable('CAN_SKIP_CHECKS', 'false');
|
||||
}
|
||||
|
||||
- name: Print PR Number and Target Commit ID
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Target Branch Name: ${{ steps.extract-pr-info.outputs.targetBranchName }}"
|
||||
echo "PR Number: ${{ steps.extract-pr-info.outputs.prNumber }}"
|
||||
echo "Target Commit ID: ${{ steps.extract-pr-info.outputs.commitId }}"
|
||||
|
||||
- name: Check if merge queue entry was enqueued as head of the queue
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
targetBranchHead=$(git rev-parse origin/${{ steps.extract-pr-info.outputs.targetBranchName }})
|
||||
if [[ "$targetBranchHead" != "${{ steps.extract-pr-info.outputs.commitId }}" ]]; then
|
||||
echo "'${{ steps.extract-pr-info.outputs.targetBranchName }}' branch commit ID does not match PR commit ID. This Merge Queue run was not head of the queue when it was enqueued. Setting CAN_SKIP_CHECKS to false."
|
||||
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "This merge queue entry is targeting '${{ steps.extract-pr-info.outputs.targetBranchName }}' directly."
|
||||
fi
|
||||
|
||||
- name: Get PR Branch
|
||||
id: get-pr-branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
prNumber=${{ steps.extract-pr-info.outputs.prNumber }}
|
||||
branchName=$(gh pr view ${prNumber} --json headRefName -q '.headRefName')
|
||||
echo "prBranch=$branchName" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Print PR Branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "PR Branch: ${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||
|
||||
- name: Check if PR branch contains the Merge Queue target commit ID
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
# Get the branch name from previous steps
|
||||
branch_name="origin/${{ steps.get-pr-branch.outputs.prBranch }}"
|
||||
commit_id="${{ steps.extract-pr-info.outputs.commitId }}"
|
||||
|
||||
# Check if the branch history contains the commit
|
||||
if git branch -r --contains "$commit_id" | grep -q "$branch_name"; then
|
||||
echo "Branch '$branch_name' contains commit '$commit_id'. It is up to date with ${{ steps.extract-pr-info.outputs.targetBranchName }}."
|
||||
else
|
||||
echo "Branch '$branch_name' does not contain commit '$commit_id'. It is outdated. Setting CAN_SKIP_CHECKS to false."
|
||||
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Compare PR Branch with Current Branch
|
||||
if: env.CAN_SKIP_CHECKS != 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
if git diff --quiet "origin/${{ steps.get-pr-branch.outputs.prBranch }}"; then
|
||||
echo "No differences found. PR branch is identical with this merge queue branch."
|
||||
else
|
||||
echo "Differences detected. PR branch has been updated after PR was added to merge queue. Setting CAN_SKIP_CHECKS to false."
|
||||
echo "CAN_SKIP_CHECKS=false" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Compute/publish skip result
|
||||
id: passed-checks
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
SECRET: ${{ inputs.secret }}
|
||||
with:
|
||||
github-token: ${{ inputs.secret != '' && inputs.secret || github.token }}
|
||||
script: |
|
||||
if (process.env.CAN_SKIP_CHECKS == "false") {
|
||||
console.log("Setting CAN_SKIP_CHECKS to false");
|
||||
core.setOutput("can-skip-checks", false);
|
||||
return;
|
||||
}
|
||||
|
||||
const secretProvided = !!process.env.SECRET;
|
||||
if (!secretProvided) {
|
||||
console.log("secret input not set, assuming all checks have passed. Ensure 'Require status checks to pass before merging' is enabled, or provide a secret with administration:read.");
|
||||
core.setOutput("can-skip-checks", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: branchProtection } = await github.rest.repos.getBranchProtection({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
branch: "${{ steps.extract-pr-info.outputs.targetBranchName }}",
|
||||
});
|
||||
const requiredCheckNames = branchProtection.required_status_checks.contexts;
|
||||
console.log(`requiredCheckNames = ${requiredCheckNames}`);
|
||||
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: "refs/heads/${{ steps.get-pr-branch.outputs.prBranch }}",
|
||||
});
|
||||
|
||||
console.log(`checks.check_runs = ${checks.check_runs.map(check => `${check.status},${check.conclusion},${check.name};`)}`);
|
||||
const nonSuccessfulChecks = checks.check_runs.filter(check => check.status !== "completed" || check.conclusion !== "success");
|
||||
const nonSuccessfulCheckNames = nonSuccessfulChecks.map(check => check.name);
|
||||
console.log(`nonSuccessfulCheckNames = ${nonSuccessfulCheckNames}`);
|
||||
|
||||
const missingChecks = requiredCheckNames.filter(checkName => nonSuccessfulCheckNames.includes(checkName));
|
||||
|
||||
if (missingChecks.length > 0) {
|
||||
console.log(`Required checks not passed, cannot skip merge queue checks. Setting CAN_SKIP_CHECKS to false. Missing checks: ${missingChecks.join(', ')}`);
|
||||
core.setOutput("can-skip-checks", false);
|
||||
} else {
|
||||
console.log("No missing checks. Setting CAN_SKIP_CHECKS to true.");
|
||||
core.setOutput("can-skip-checks", true);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
name: API client release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/api-client-release.yml
|
||||
- packages/api-client/**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository_owner == 'modrinth' && github.ref == 'refs/heads/main'
|
||||
# npm Trusted Publishing requires a GitHub-hosted runner.
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FORCE_COLOR: 3
|
||||
PACKAGE_DIR: packages/api-client
|
||||
PACKAGE_NAME: '@modrinth/api-client'
|
||||
BUMP_TYPE: minor
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check for api-client changes
|
||||
id: changes
|
||||
run: |
|
||||
if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if git diff --quiet "${{ github.event.before }}" "$GITHUB_SHA" -- "$PACKAGE_DIR"; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Enable Corepack
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Restore pnpm cache
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: pnpm install --frozen-lockfile --filter @modrinth/api-client...
|
||||
|
||||
- name: Resolve release version
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
id: version
|
||||
run: |
|
||||
CURRENT_VERSION_JSON="$(npm view "${PACKAGE_NAME}" version --json)"
|
||||
CURRENT_VERSION="$(
|
||||
jq -nr \
|
||||
--argjson version "$CURRENT_VERSION_JSON" \
|
||||
'if ($version | type) == "array" then $version[-1] else $version end'
|
||||
)"
|
||||
|
||||
NEXT_VERSION="$(
|
||||
jq -nr \
|
||||
--arg version "$CURRENT_VERSION" \
|
||||
--arg bump "$BUMP_TYPE" '
|
||||
def semver:
|
||||
capture("^(?<major>[0-9]+)\\.(?<minor>[0-9]+)\\.(?<patch>[0-9]+)$")
|
||||
| with_entries(.value |= tonumber);
|
||||
|
||||
($version | semver) as $current
|
||||
| if $bump == "major" then "\($current.major + 1).0.0"
|
||||
elif $bump == "minor" then "\($current.major).\($current.minor + 1).0"
|
||||
elif $bump == "patch" then "\($current.major).\($current.minor).\($current.patch + 1)"
|
||||
else error("Unsupported bump type: \($bump)")
|
||||
end
|
||||
'
|
||||
)"
|
||||
|
||||
PACKAGE_JSON="$(mktemp)"
|
||||
jq --tab --arg version "$NEXT_VERSION" '.version = $version' "$PACKAGE_DIR/package.json" > "$PACKAGE_JSON"
|
||||
mv "$PACKAGE_JSON" "$PACKAGE_DIR/package.json"
|
||||
|
||||
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "published_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build api-client
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
run: pnpm --filter @modrinth/api-client build
|
||||
|
||||
- name: Check package contents
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: packages/api-client
|
||||
run: pnpm pack --dry-run
|
||||
|
||||
- name: Publish api-client
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
working-directory: packages/api-client
|
||||
run: pnpm publish --access public --provenance --no-git-checks
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Cancel PR Workflows on Merge
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
cancel:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1
|
||||
with:
|
||||
workflow_id: all
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ignore_sha: true
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
@@ -2,7 +2,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -2,7 +2,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -3,7 +3,7 @@ name: daedalus-docker-build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- 'main'
|
||||
paths:
|
||||
- .github/workflows/daedalus-docker.yml
|
||||
- 'apps/daedalus_client/**'
|
||||
@@ -26,19 +26,66 @@ concurrency:
|
||||
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: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
env:
|
||||
SCCACHE_DIR: '/mnt/sccache'
|
||||
SCCACHE_CACHE_SIZE: '10G'
|
||||
SCCACHE_MULTILEVEL_CHAIN: 'disk,s3'
|
||||
SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/'
|
||||
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: 'sccache'
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -59,12 +106,14 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -3,7 +3,7 @@ name: docker-build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
- 'main'
|
||||
paths:
|
||||
- .github/workflows/labrinth-docker.yml
|
||||
- 'apps/labrinth/**'
|
||||
@@ -24,21 +24,68 @@ concurrency:
|
||||
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: blacksmith-4vcpu-ubuntu-2404
|
||||
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: '/mnt/sccache'
|
||||
SCCACHE_CACHE_SIZE: '10G'
|
||||
SCCACHE_MULTILEVEL_CHAIN: 'disk,s3'
|
||||
SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/'
|
||||
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: 'sccache'
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -62,12 +109,14 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -13,6 +13,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -53,18 +53,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
[
|
||||
platform: [
|
||||
blacksmith-6vcpu-macos-26,
|
||||
blacksmith-8vcpu-windows-2025,
|
||||
blacksmith-8vcpu-ubuntu-2404,
|
||||
blacksmith-8vcpu-windows-2025, # At time of writing, Windows 4 vCPU VMs don't seem to actually exist
|
||||
blacksmith-4vcpu-ubuntu-2404,
|
||||
]
|
||||
include:
|
||||
- platform: blacksmith-6vcpu-macos-26
|
||||
artifact-target-name: universal-apple-darwin
|
||||
- platform: blacksmith-8vcpu-windows-2025
|
||||
artifact-target-name: x86_64-pc-windows-msvc
|
||||
- platform: blacksmith-8vcpu-ubuntu-2404
|
||||
- platform: blacksmith-4vcpu-ubuntu-2404
|
||||
artifact-target-name: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
@@ -157,6 +156,7 @@ jobs:
|
||||
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: contains(matrix.platform, 'macos')
|
||||
env:
|
||||
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
startsWith(github.event.workflow_run.head_branch, 'v')
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
VERSION_TAG: ${{ github.event.workflow_run.head_branch }}
|
||||
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
|
||||
|
||||
@@ -13,9 +13,58 @@ concurrency:
|
||||
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: blacksmith-4vcpu-ubuntu-2404
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
|
||||
env:
|
||||
# Ensure pnpm output is colored in GitHub Actions logs
|
||||
@@ -27,17 +76,19 @@ jobs:
|
||||
# since we don't want warnings to become errors
|
||||
# while developing)
|
||||
RUSTFLAGS: -Dwarnings
|
||||
# sccache config
|
||||
SCCACHE_DIR: '/mnt/sccache'
|
||||
SCCACHE_CACHE_SIZE: '10G'
|
||||
SCCACHE_MULTILEVEL_CHAIN: 'disk,s3'
|
||||
SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/'
|
||||
# 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: 'sccache'
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
@@ -88,12 +139,14 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -51,9 +51,10 @@ import {
|
||||
providePageContext,
|
||||
providePopupNotificationManager,
|
||||
useDebugLogger,
|
||||
useFormatBytes,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { formatBytes, renderString } from '@modrinth/utils'
|
||||
import { 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'
|
||||
@@ -146,8 +147,9 @@ const popupNotificationManager = new AppPopupNotificationManager()
|
||||
providePopupNotificationManager(popupNotificationManager)
|
||||
const { addPopupNotification } = popupNotificationManager
|
||||
|
||||
const appVersion = getVersion()
|
||||
const tauriApiClient = new TauriModrinthClient({
|
||||
userAgent: `modrinth/theseus/${getVersion()} (support@modrinth.com)`,
|
||||
userAgent: async () => `modrinth/theseus/${await appVersion} (support@modrinth.com)`,
|
||||
labrinthBaseUrl: config.labrinthBaseUrl,
|
||||
archonBaseUrl: config.archonBaseUrl,
|
||||
features: [
|
||||
@@ -261,6 +263,8 @@ onUnmounted(async () => {
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const messages = defineMessages({
|
||||
updateInstalledToastTitle: {
|
||||
id: 'app.update.complete-toast.title',
|
||||
@@ -1177,7 +1181,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<div id="teleports"></div>
|
||||
<div
|
||||
v-if="stateInitialized"
|
||||
class="app-grid-layout experimental-styles-within relative"
|
||||
class="app-grid-layout relative"
|
||||
:class="{ 'disable-advanced-rendering': !themeStore.advancedRendering }"
|
||||
>
|
||||
<Transition name="fade">
|
||||
@@ -1378,7 +1382,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</div>
|
||||
<div
|
||||
v-if="stateInitialized"
|
||||
class="app-contents experimental-styles-within"
|
||||
class="app-contents"
|
||||
:class="{
|
||||
'sidebar-enabled': sidebarVisible,
|
||||
'disable-advanced-rendering': !themeStore.advancedRendering,
|
||||
@@ -1472,7 +1476,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" mode="small" />
|
||||
<AccountsCard ref="accounts" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid">
|
||||
@@ -1567,11 +1571,15 @@ 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] {
|
||||
@@ -1657,10 +1665,11 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.app-contents::before {
|
||||
z-index: 1;
|
||||
z-index: 30;
|
||||
content: '';
|
||||
position: fixed;
|
||||
left: var(--left-bar-width);
|
||||
|
||||
@@ -149,7 +149,7 @@ const handleOptionsClick = async (args) => {
|
||||
break
|
||||
case 'edit':
|
||||
await router.push({
|
||||
path: `/instance/${encodeURIComponent(args.item.path)}/`,
|
||||
path: `/instance/${encodeURIComponent(args.item.path)}`,
|
||||
})
|
||||
break
|
||||
case 'duplicate':
|
||||
|
||||
@@ -1,81 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="mode !== 'isolated'"
|
||||
ref="button"
|
||||
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||
:class="{ expanded: mode === 'expanded' }"
|
||||
@click="toggleMenu"
|
||||
v-if="accounts.length === 0"
|
||||
class="flex flex-col gap-3 bg-button-bg border border-solid border-surface-5 rounded-xl p-3 mt-2"
|
||||
>
|
||||
<Avatar
|
||||
size="36px"
|
||||
:src="
|
||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col w-full">
|
||||
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
||||
<span class="text-secondary text-xs">Minecraft account</span>
|
||||
</div>
|
||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||
<span>{{ formatMessage(messages.notSignedIn) }}</span>
|
||||
<ButtonStyled color="brand">
|
||||
<button color="primary" :disabled="loginDisabled" @click="login()">
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
{{ formatMessage(messages.signInToMinecraft) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<Card
|
||||
v-if="showCard || mode === 'isolated'"
|
||||
ref="card"
|
||||
class="account-card"
|
||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||
>
|
||||
<div v-if="selectedAccount" class="selected account">
|
||||
<Avatar size="xs" :src="avatarUrl" />
|
||||
<div>
|
||||
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||
<p>Selected</p>
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="'Log out'"
|
||||
icon-only
|
||||
color="raised"
|
||||
@click="logout(selectedAccount.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="logged-out account">
|
||||
<h4>Not signed in</h4>
|
||||
<Button
|
||||
v-tooltip="'Log in'"
|
||||
:disabled="loginDisabled"
|
||||
icon-only
|
||||
color="primary"
|
||||
@click="login()"
|
||||
>
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||
<Button class="option account" @click="setAccount(account)">
|
||||
<Avatar :src="getAccountAvatarUrl(account)" class="icon" />
|
||||
<p>{{ account.profile.name }}</p>
|
||||
</Button>
|
||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
<Accordion
|
||||
v-else
|
||||
class="w-full mt-2 bg-button-bg border border-solid border-surface-5 rounded-xl overflow-clip"
|
||||
button-class="button-base w-full bg-transparent px-3 py-2 border-0 cursor-pointer"
|
||||
:open-by-default="false"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex gap-2 w-full min-w-0">
|
||||
<Avatar
|
||||
size="36px"
|
||||
:src="
|
||||
selectedAccount
|
||||
? avatarUrl
|
||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col items-start w-full min-w-0">
|
||||
<span class="truncate w-full text-left">{{
|
||||
selectedAccount ? selectedAccount.profile.name : formatMessage(messages.selectAccount)
|
||||
}}</span>
|
||||
<span class="text-secondary text-xs">{{ formatMessage(messages.minecraftAccount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button v-if="accounts.length > 0" @click="login()">
|
||||
<PlusIcon />
|
||||
Add account
|
||||
</Button>
|
||||
</Card>
|
||||
</transition>
|
||||
</template>
|
||||
<div class="bg-button-bg pt-1 pb-2 border border-solid border-surface-5">
|
||||
<template v-if="accounts.length > 0">
|
||||
<div v-for="account in accounts" :key="account.profile.id" class="flex gap-1 items-center">
|
||||
<button
|
||||
class="flex items-center flex-shrink flex-grow overflow-clip gap-2 p-2 border-0 bg-transparent cursor-pointer button-base min-w-0"
|
||||
@click="setAccount(account)"
|
||||
>
|
||||
<RadioButtonCheckedIcon
|
||||
v-if="selectedAccount && selectedAccount.profile.id === account.profile.id"
|
||||
class="w-5 h-5 text-brand shrink-0"
|
||||
/>
|
||||
<RadioButtonIcon v-else class="w-5 h-5 text-secondary shrink-0" />
|
||||
<Avatar :src="getAccountAvatarUrl(account)" size="24px" />
|
||||
<p
|
||||
class="m-0 truncate min-w-0"
|
||||
:class="
|
||||
selectedAccount && selectedAccount.profile.id === account.profile.id
|
||||
? 'text-contrast font-semibold'
|
||||
: 'text-primary'
|
||||
"
|
||||
>
|
||||
{{ account.profile.name }}
|
||||
</p>
|
||||
</button>
|
||||
<ButtonStyled circular color="red" color-fill="none" hover-color-fill="background">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.removeAccount)"
|
||||
class="mr-2"
|
||||
@click="logout(account.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col gap-2 px-2 pt-2">
|
||||
<ButtonStyled v-if="accounts.length > 0" class="w-full">
|
||||
<button :disabled="loginDisabled" @click="login()">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addAccount) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DropdownIcon, LogInIcon, PlusIcon, SpinnerIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LogInIcon,
|
||||
PlusIcon,
|
||||
RadioButtonCheckedIcon,
|
||||
RadioButtonIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import {
|
||||
@@ -87,34 +113,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()
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: 'normal',
|
||||
},
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
change: []
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
type MinecraftCredential = {
|
||||
profile: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = ref({})
|
||||
const accounts: Ref<MinecraftCredential[]> = ref([])
|
||||
const loginDisabled = ref(false)
|
||||
const defaultUser = ref()
|
||||
const equippedSkin = ref(null)
|
||||
const headUrlCache = ref(new Map())
|
||||
const defaultUser = ref<string | undefined>()
|
||||
const equippedSkin = ref<Skin | null>(null)
|
||||
const headUrlCache = ref(new Map<string, string>())
|
||||
|
||||
async function refreshValues() {
|
||||
defaultUser.value = await get_default_user().catch(handleError)
|
||||
accounts.value = await users().catch(handleError)
|
||||
const userList = await users().catch(handleError)
|
||||
accounts.value = Array.isArray(userList) ? [...userList] : []
|
||||
accounts.value.sort((a, b) => (a.profile?.name ?? '').localeCompare(b.profile?.name ?? ''))
|
||||
|
||||
try {
|
||||
const skins = await get_available_skins()
|
||||
equippedSkin.value = skins.find((skin) => skin.is_equipped)
|
||||
equippedSkin.value = skins.find((skin) => skin.is_equipped) ?? null
|
||||
|
||||
if (equippedSkin.value) {
|
||||
try {
|
||||
@@ -129,7 +160,7 @@ async function refreshValues() {
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginDisabled(value) {
|
||||
function setLoginDisabled(value: boolean) {
|
||||
loginDisabled.value = value
|
||||
}
|
||||
|
||||
@@ -138,10 +169,11 @@ defineExpose({
|
||||
setLoginDisabled,
|
||||
loginDisabled,
|
||||
})
|
||||
|
||||
await refreshValues()
|
||||
|
||||
const displayAccounts = computed(() =>
|
||||
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
@@ -158,7 +190,7 @@ const avatarUrl = computed(() => {
|
||||
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
})
|
||||
|
||||
function getAccountAvatarUrl(account) {
|
||||
function getAccountAvatarUrl(account: MinecraftCredential) {
|
||||
if (
|
||||
account.profile.id === selectedAccount.value?.profile?.id &&
|
||||
equippedSkin.value?.texture_key
|
||||
@@ -171,13 +203,10 @@ function getAccountAvatarUrl(account) {
|
||||
return `https://mc-heads.net/avatar/${account.profile.id}/128`
|
||||
}
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
|
||||
async function setAccount(account) {
|
||||
async function setAccount(account: MinecraftCredential) {
|
||||
defaultUser.value = account.profile.id
|
||||
await set_default_user(account.profile.id).catch(handleError)
|
||||
await refreshValues()
|
||||
emit('change')
|
||||
}
|
||||
|
||||
@@ -187,292 +216,57 @@ async function login() {
|
||||
|
||||
if (loggedIn) {
|
||||
await setAccount(loggedIn)
|
||||
await refreshValues()
|
||||
}
|
||||
|
||||
trackEvent('AccountLogIn')
|
||||
loginDisabled.value = false
|
||||
}
|
||||
|
||||
const logout = async (id) => {
|
||||
async function logout(id: string) {
|
||||
await remove_user(id).catch(handleError)
|
||||
await refreshValues()
|
||||
if (!selectedAccount.value && accounts.value.length > 0) {
|
||||
await setAccount(accounts.value[0])
|
||||
await refreshValues()
|
||||
} else {
|
||||
emit('change')
|
||||
}
|
||||
trackEvent('AccountLogOut')
|
||||
}
|
||||
|
||||
const showCard = ref(false)
|
||||
const card = ref(null)
|
||||
const button = ref(null)
|
||||
const handleClickOutside = (event) => {
|
||||
const elements = document.elementsFromPoint(event.clientX, event.clientY)
|
||||
if (
|
||||
card.value &&
|
||||
card.value.$el !== event.target &&
|
||||
!elements.includes(card.value.$el) &&
|
||||
!button.value.contains(event.target)
|
||||
) {
|
||||
toggleMenu(false)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMenu(override = true) {
|
||||
if (showCard.value || !override) {
|
||||
showCard.value = false
|
||||
} else {
|
||||
showCard.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const unlisten = await process_listener(async (e) => {
|
||||
if (e.event === 'launched') {
|
||||
await refreshValues()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
notSignedIn: {
|
||||
id: 'minecraft-account.not-signed-in',
|
||||
defaultMessage: 'Not signed in',
|
||||
},
|
||||
addAccount: {
|
||||
id: 'minecraft-account.add-account',
|
||||
defaultMessage: 'Add account',
|
||||
},
|
||||
removeAccount: {
|
||||
id: 'minecraft-account.remove-account',
|
||||
defaultMessage: 'Remove account',
|
||||
},
|
||||
selectAccount: {
|
||||
id: 'minecraft-account.select-account',
|
||||
defaultMessage: 'Select account',
|
||||
},
|
||||
minecraftAccount: {
|
||||
id: 'minecraft-account.label',
|
||||
defaultMessage: 'Minecraft account',
|
||||
},
|
||||
signInToMinecraft: {
|
||||
id: 'minecraft-account.sign-in',
|
||||
defaultMessage: 'Sign in to Minecraft',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.selected {
|
||||
background: var(--color-brand-highlight);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--color-contrast);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logged-out {
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.account {
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
h4,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.account-card {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.5rem;
|
||||
right: 2rem;
|
||||
z-index: 11;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-divider);
|
||||
width: max-content;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
border-top-right-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
border-top-right-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
left: 13.5rem;
|
||||
}
|
||||
|
||||
&.isolated {
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.accounts-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.account-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.option {
|
||||
width: calc(100% - 2.25rem);
|
||||
background: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
box-shadow: none;
|
||||
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
--size: 1.5rem !important;
|
||||
}
|
||||
|
||||
.account-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
vertical-align: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
translate 0.25s ease,
|
||||
scale 0.25s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
translate: 0 -2rem;
|
||||
scale: 0.9;
|
||||
}
|
||||
|
||||
.avatar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-base);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--radius-md);
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
|
||||
&.expanded {
|
||||
border: 1px solid var(--color-divider);
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
margin: auto 0 auto 0.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 6rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.accounts-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
background-color: white !important;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-lg);
|
||||
align-items: center;
|
||||
padding: var(--gap-xl);
|
||||
|
||||
.modal-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
width: 100%;
|
||||
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.code-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--gap-xs);
|
||||
align-items: center;
|
||||
|
||||
.code {
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--radius-md);
|
||||
border: solid 1px var(--color-button-bg);
|
||||
font-family: var(--mono-font);
|
||||
letter-spacing: var(--gap-md);
|
||||
color: var(--color-contrast);
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
padding: var(--gap-sm) 0 var(--gap-sm) var(--gap-md);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.code {
|
||||
color: var(--color-brand);
|
||||
padding: 0.05rem 0.1rem;
|
||||
// row not column
|
||||
display: flex;
|
||||
|
||||
.card {
|
||||
background: var(--color-base);
|
||||
color: var(--color-contrast);
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { WrenchIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||
@@ -40,9 +41,13 @@ const messages = defineMessages({
|
||||
},
|
||||
selectFilesLabel: {
|
||||
id: 'app.export-modal.select-files-label',
|
||||
defaultMessage: 'Select files and folders to include in pack',
|
||||
defaultMessage: 'Configure which files are included in this export',
|
||||
},
|
||||
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||
includeFile: {
|
||||
id: 'app.export-modal.include-file-accessibility-label',
|
||||
defaultMessage: 'Include "{file}"?',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -65,7 +70,6 @@ const exportDescription = ref('')
|
||||
const versionInput = ref('1.0.0')
|
||||
const files = ref([])
|
||||
const folders = ref([])
|
||||
const showingFiles = ref(false)
|
||||
|
||||
const initFiles = async () => {
|
||||
const newFolders = new Map()
|
||||
@@ -87,7 +91,12 @@ const initFiles = async () => {
|
||||
folder.startsWith('modrinth_logs') ||
|
||||
folder.startsWith('.fabric'),
|
||||
}))
|
||||
.filter((pathData) => !pathData.path.includes('.DS_Store'))
|
||||
.filter(
|
||||
(pathData) =>
|
||||
!pathData.path.includes('.DS_Store') &&
|
||||
pathData.path !== 'mods/.connector' &&
|
||||
!pathData.path.startsWith('mods/.connector/'),
|
||||
)
|
||||
.forEach((pathData) => {
|
||||
const parent = pathData.path.split(sep).slice(0, -1).join(sep)
|
||||
if (parent !== '') {
|
||||
@@ -121,15 +130,20 @@ const exportPack = async () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
const outputPath = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
const outputPath = await save({
|
||||
defaultPath: `${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
filters: [
|
||||
{
|
||||
name: 'Modrinth Modpack',
|
||||
extensions: ['mrpack'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (outputPath) {
|
||||
export_profile_mrpack(
|
||||
props.instance.path,
|
||||
outputPath + `/${nameInput.value} ${versionInput.value}.mrpack`,
|
||||
outputPath,
|
||||
filesToExport,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
@@ -142,188 +156,104 @@ const exportPack = async () => {
|
||||
|
||||
<template>
|
||||
<ModalWrapper ref="exportModal" :header="formatMessage(messages.header)">
|
||||
<div class="modal-body">
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<div class="flex flex-col gap-4 w-[40rem]">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||
|
||||
<p>{{ formatMessage(messages.modpackNameLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||
v-model="nameInput"
|
||||
:icon="PackageIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.modpackNamePlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
<div class="labeled_input">
|
||||
<p>{{ formatMessage(messages.versionNumberLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="versionInput"
|
||||
:icon="VersionIcon"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.versionNumberPlaceholder)"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table">
|
||||
<div class="table-head">
|
||||
<div class="table-cell row-wise">
|
||||
{{ formatMessage(messages.selectFilesLabel) }}
|
||||
<Button
|
||||
class="sleek-primary collapsed-button"
|
||||
icon-only
|
||||
@click="() => (showingFiles = !showingFiles)"
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">{{ formatMessage(commonMessages.descriptionLabel) }}</p>
|
||||
<StyledInput
|
||||
v-model="exportDescription"
|
||||
multiline
|
||||
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
<Accordion
|
||||
class="w-full bg-surface-4 border border-solid border-surface-5 rounded-2xl overflow-clip"
|
||||
button-class="p-4 w-full border-b border-solid border-b-surface-5 bg-surface-2 -mb-px hover:brightness-[--hover-brightness] group"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-3 text-contrast group-active:scale-[0.98]">
|
||||
<WrenchIcon aria-hidden="true" class="size-5 text-secondary" />
|
||||
Configure which files are included in this export
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col [&>*:nth-child(even)]:bg-surface-3">
|
||||
<div v-for="[path, children] in folders" :key="path.name" class="flex flex-col">
|
||||
<Accordion
|
||||
class="flex flex-col"
|
||||
button-class="flex gap-3 pr-4 hover:bg-surface-5 group"
|
||||
>
|
||||
<PlusIcon v-if="!showingFiles" />
|
||||
<XIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showingFiles" class="table-content">
|
||||
<div v-for="[path, children] in folders" :key="path.name" class="table-row">
|
||||
<div class="table-cell file-entry">
|
||||
<div class="file-primary">
|
||||
<template #title>
|
||||
<Checkbox
|
||||
:model-value="children.every((child) => child.selected)"
|
||||
:label="path.name"
|
||||
class="select-checkbox"
|
||||
:indeterminate="
|
||||
!children.every((child) => child.selected) &&
|
||||
children.some((child) => child.selected)
|
||||
"
|
||||
:description="formatMessage(messages.includeFile, { file: path.name })"
|
||||
class="pl-4 py-2"
|
||||
:disabled="children.every((x) => x.disabled)"
|
||||
@update:model-value="
|
||||
(newValue) => children.forEach((child) => (child.selected = newValue))
|
||||
"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="ml-2 group-active:scale-95">{{ path.name }}/</span>
|
||||
</template>
|
||||
<div v-for="child in children" :key="child.path">
|
||||
<Checkbox
|
||||
v-model="path.showingMore"
|
||||
class="select-checkbox dropdown"
|
||||
collapsing-toggle-style
|
||||
v-model="child.selected"
|
||||
:label="child.name"
|
||||
class="w-full px-8 py-2 hover:bg-surface-4 text-primary"
|
||||
:disabled="child.disabled"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="path.showingMore" class="file-secondary">
|
||||
<div v-for="child in children" :key="child.path" class="file-secondary-row">
|
||||
<Checkbox
|
||||
v-model="child.selected"
|
||||
:label="child.name"
|
||||
class="select-checkbox"
|
||||
:disabled="child.disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="file in files" :key="file.path" class="table-row">
|
||||
<div class="table-cell file-entry">
|
||||
<div class="file-primary">
|
||||
<Checkbox
|
||||
v-model="file.selected"
|
||||
:label="file.name"
|
||||
:disabled="file.disabled"
|
||||
class="select-checkbox"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
||||
<Checkbox
|
||||
v-for="file in files"
|
||||
:key="file.path"
|
||||
v-model="file.selected"
|
||||
:label="file.name"
|
||||
:disabled="file.disabled"
|
||||
class="w-full px-4 py-2 hover:bg-surface-4 text-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row push-right">
|
||||
<Button @click="exportModal.hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button color="primary" @click="exportPack">
|
||||
<PackageIcon />
|
||||
{{ formatMessage(messages.exportButton) }}
|
||||
</Button>
|
||||
</Accordion>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="exportModal.hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="exportPack">
|
||||
<PackageIcon />
|
||||
{{ formatMessage(messages.exportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
}
|
||||
|
||||
.labeled_input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.select-checkbox {
|
||||
gap: var(--gap-sm);
|
||||
|
||||
button.checkbox {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.table-content {
|
||||
max-height: 18rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
border: 1px solid var(--color-bg);
|
||||
}
|
||||
|
||||
.file-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.file-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.file-secondary {
|
||||
margin-left: var(--gap-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
height: 100%;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.file-secondary-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-wise {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
<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>
|
||||
<ButtonStyled v-if="currentSelected.path === javaInstall.path">
|
||||
<button disabled><CheckIcon /> Selected</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button @click="setJavaInstall(javaInstall)"><PlusIcon /> Select</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="chosenInstallOptions.length === 0" class="table-row entire-row">
|
||||
@@ -26,17 +28,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<Button @click="$refs.detectJavaModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="$refs.detectJavaModal.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
<script setup>
|
||||
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
@@ -17,35 +17,45 @@
|
||||
"
|
||||
/>
|
||||
<span class="installation-buttons">
|
||||
<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>
|
||||
<ButtonStyled v-if="props.version">
|
||||
<button :disabled="props.disabled || installingJava" @click="reinstallJava">
|
||||
<DownloadIcon />
|
||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="testingJava">
|
||||
<button disabled>Testing...</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="testingJavaSuccess === true">
|
||||
<button disabled>
|
||||
<CheckIcon />
|
||||
Success
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="testingJavaSuccess === false">
|
||||
<button disabled>
|
||||
<XIcon />
|
||||
Failed
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button :disabled="props.disabled" @click="testJava">
|
||||
<PlayIcon />
|
||||
Test
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,7 +69,7 @@ import {
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -204,10 +214,6 @@ async function reinstallJava() {
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
|
||||
.btn {
|
||||
width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.test-success {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { CheckIcon } from '@modrinth/assets'
|
||||
import { Badge, Button } from '@modrinth/ui'
|
||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
@@ -74,15 +74,18 @@ const onHide = () => {
|
||||
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<Button
|
||||
:color="version.id === installedVersion ? '' : 'primary'"
|
||||
icon-only
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
<ButtonStyled
|
||||
circular
|
||||
:color="version.id === installedVersion ? 'standard' : 'brand'"
|
||||
>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</Button>
|
||||
<button
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="name-cell table-cell table-text">
|
||||
<div class="version-link">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Button, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -12,7 +12,6 @@ const { install: installVersion } = injectContentInstall()
|
||||
const confirmModal = ref(null)
|
||||
const project = ref(null)
|
||||
const version = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
defineExpose({
|
||||
async show(event) {
|
||||
@@ -70,7 +69,9 @@ async function install() {
|
||||
</p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button :loading="installing" color="primary" @click="install">Install</Button>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="install">Install</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,10 +34,14 @@
|
||||
</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>
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="() => incompatibleModal.hide()"><XIcon />Cancel</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="installing" @click="install()">
|
||||
<DownloadIcon /> {{ installing ? 'Installing' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
@@ -45,7 +49,13 @@
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, Combobox, formatLoader, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Combobox,
|
||||
formatLoader,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
<script setup>
|
||||
import {
|
||||
CheckIcon,
|
||||
DownloadIcon,
|
||||
PlusIcon,
|
||||
RightArrowIcon,
|
||||
SearchIcon,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, Card, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_v3_many } from '@/helpers/cache.js'
|
||||
import {
|
||||
add_project_from_version as installMod,
|
||||
check_installed,
|
||||
create,
|
||||
get,
|
||||
list,
|
||||
} from '@/helpers/profile'
|
||||
import {
|
||||
findPreferredVersion,
|
||||
installVersionDependencies,
|
||||
isVersionCompatible,
|
||||
} from '@/store/install.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const router = useRouter()
|
||||
|
||||
const versions = ref()
|
||||
const project = ref()
|
||||
|
||||
const installModal = ref()
|
||||
const searchFilter = ref('')
|
||||
|
||||
const showCreation = ref(false)
|
||||
const icon = ref(null)
|
||||
const name = ref(null)
|
||||
const display_icon = ref(null)
|
||||
const loader = ref(null)
|
||||
const gameVersion = ref(null)
|
||||
const creatingInstance = ref(false)
|
||||
|
||||
const profiles = ref([])
|
||||
|
||||
const shownProfiles = computed(() =>
|
||||
profiles.value.filter((profile) => {
|
||||
return profile.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
}),
|
||||
)
|
||||
|
||||
const isProfileCompatible = (profile) =>
|
||||
versions.value?.some((version) => isVersionCompatible(version, project.value, profile))
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: async (projectVal, versionsVal, callback) => {
|
||||
project.value = projectVal
|
||||
versions.value = versionsVal
|
||||
searchFilter.value = ''
|
||||
|
||||
showCreation.value = false
|
||||
name.value = null
|
||||
icon.value = null
|
||||
display_icon.value = null
|
||||
gameVersion.value = null
|
||||
loader.value = null
|
||||
|
||||
onInstall.value = callback
|
||||
|
||||
const profilesVal = await list().catch(handleError)
|
||||
for (const profile of profilesVal) {
|
||||
profile.installing = false
|
||||
profile.installedMod = await check_installed(profile.path, project.value.id).catch(
|
||||
handleError,
|
||||
)
|
||||
}
|
||||
|
||||
const linkedProjectIds = profilesVal
|
||||
.filter((p) => p.linked_data?.project_id)
|
||||
.map((p) => p.linked_data.project_id)
|
||||
if (linkedProjectIds.length > 0) {
|
||||
const linkedProjects = await get_project_v3_many(linkedProjectIds, 'must_revalidate').catch(
|
||||
() => [],
|
||||
)
|
||||
const serverProjectIds = new Set(
|
||||
linkedProjects.filter((p) => p?.minecraft_server != null).map((p) => p.id),
|
||||
)
|
||||
for (const profile of profilesVal) {
|
||||
profile.isServerInstance = serverProjectIds.has(profile.linked_data?.project_id)
|
||||
}
|
||||
}
|
||||
|
||||
profiles.value = profilesVal
|
||||
|
||||
installModal.value.show()
|
||||
|
||||
trackEvent('ProjectInstallStart', { source: 'ProjectInstallModal' })
|
||||
},
|
||||
})
|
||||
|
||||
async function install(instance) {
|
||||
instance.installing = true
|
||||
const version = findPreferredVersion(versions.value, project.value, instance)
|
||||
|
||||
if (!version) {
|
||||
instance.installing = false
|
||||
handleError('No compatible version found')
|
||||
return
|
||||
}
|
||||
|
||||
await installMod(instance.path, version.id, 'standalone').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, 'standalone').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>
|
||||
@@ -7,7 +7,7 @@
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="modal?.hide()">
|
||||
<button @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="handleCancel">
|
||||
<button @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
@@ -73,9 +73,11 @@ async function findLauncherDir() {
|
||||
wrapper-class="w-full"
|
||||
>
|
||||
<template #right>
|
||||
<Button class="ml-1.5" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</Button>
|
||||
<ButtonStyled circular>
|
||||
<button class="ml-1.5" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</StyledInput>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
||||
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
||||
<ButtonStyled>
|
||||
<button @click="openUploadSkinModal"><UploadIcon /> Replace texture</button>
|
||||
</ButtonStyled>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -79,7 +81,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-12">
|
||||
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
||||
<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'" />
|
||||
@@ -87,7 +89,9 @@
|
||||
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
||||
<ButtonStyled type="outlined">
|
||||
<button :disabled="isSaving" @click="hide"><XIcon />Cancel</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
@@ -109,7 +113,6 @@ import {
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
|
||||
@@ -93,7 +93,7 @@ defineExpose({ show, hide })
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="hide()">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -106,7 +106,7 @@ const titleMessage = defineMessage({
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border !border-surface-4" @click="hide()">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { CheckIcon, PlayIcon, PlusIcon, StopCircleIcon } from '@modrinth/assets'
|
||||
import type { CardAction } from '@modrinth/ui'
|
||||
import { commonMessages, defineMessages, useDebugLogger, useVIntl } from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import { kill, list as listInstances } from '@/helpers/profile.js'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { add_server_to_profile, getServerLatency } from '@/helpers/worlds'
|
||||
import { getServerAddress } from '@/store/install.js'
|
||||
|
||||
interface BrowseServerInstance {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
|
||||
interface ContextMenuHandle {
|
||||
showMenu: (
|
||||
event: MouseEvent,
|
||||
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
options: { name: string }[],
|
||||
) => void
|
||||
}
|
||||
|
||||
interface ContextMenuOptionClick {
|
||||
option: 'open_link' | 'copy_link'
|
||||
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject
|
||||
}
|
||||
|
||||
export interface UseAppServerBrowseOptions {
|
||||
instance: Ref<BrowseServerInstance | null>
|
||||
isFromWorlds: ComputedRef<boolean>
|
||||
allInstalledIds: ComputedRef<Set<string>>
|
||||
newlyInstalled: Ref<string[]>
|
||||
installingServerProjects: Ref<string[]>
|
||||
playServerProject: (projectId: string) => Promise<void>
|
||||
showAddServerToInstanceModal: (serverName: string, serverAddress: string) => void
|
||||
handleError: (error: unknown) => void
|
||||
router: Router
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
addToInstance: {
|
||||
id: 'app.browse.add-to-instance',
|
||||
defaultMessage: 'Add to instance',
|
||||
},
|
||||
addToInstanceName: {
|
||||
id: 'app.browse.add-to-instance-name',
|
||||
defaultMessage: 'Add to {instanceName}',
|
||||
},
|
||||
added: {
|
||||
id: 'app.browse.added',
|
||||
defaultMessage: 'Added',
|
||||
},
|
||||
alreadyAdded: {
|
||||
id: 'app.browse.already-added',
|
||||
defaultMessage: 'Already added',
|
||||
},
|
||||
})
|
||||
|
||||
export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const debugLog = useDebugLogger('BrowseServer')
|
||||
const serverPings = shallowRef<Record<string, number | undefined>>({})
|
||||
const serverPingCache = new Map<string, number | undefined>()
|
||||
const pendingServerPings = new Map<string, Promise<number | undefined>>()
|
||||
const runningServerProjects = ref<Record<string, string>>({})
|
||||
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
|
||||
const contextMenuRef = ref<ContextMenuHandle | null>(null)
|
||||
let serverPingCacheActive = true
|
||||
let unlistenProcesses: (() => void) | null = null
|
||||
|
||||
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('checkServerRunningStates', { hitCount: hits.length })
|
||||
const packs = await listInstances().catch((error) => {
|
||||
options.handleError(error)
|
||||
return []
|
||||
})
|
||||
const newRunning: Record<string, string> = {}
|
||||
for (const hit of hits) {
|
||||
const inst = packs.find(
|
||||
(pack: GameInstance) => pack.linked_data?.project_id === hit.project_id,
|
||||
)
|
||||
if (inst) {
|
||||
const processes = await get_by_profile_path(inst.path).catch(() => [])
|
||||
if (Array.isArray(processes) && processes.length > 0) {
|
||||
newRunning[hit.project_id] = inst.path
|
||||
}
|
||||
}
|
||||
}
|
||||
debugLog('runningServerProjects updated', newRunning)
|
||||
runningServerProjects.value = newRunning
|
||||
}
|
||||
|
||||
async function handleStopServerProject(projectId: string) {
|
||||
debugLog('handleStopServerProject', projectId)
|
||||
const instancePath = runningServerProjects.value[projectId]
|
||||
if (!instancePath) return
|
||||
await kill(instancePath).catch(() => {})
|
||||
const { [projectId]: _, ...rest } = runningServerProjects.value
|
||||
runningServerProjects.value = rest
|
||||
}
|
||||
|
||||
async function handlePlayServerProject(projectId: string) {
|
||||
debugLog('handlePlayServerProject', projectId)
|
||||
await options.playServerProject(projectId)
|
||||
checkServerRunningStates(lastServerHits.value)
|
||||
}
|
||||
|
||||
async function handleAddServerToInstance(project: Labrinth.Search.v3.ResultSearchProject) {
|
||||
debugLog('handleAddServerToInstance', { projectId: project.project_id, name: project.name })
|
||||
const address = getServerAddress(project.minecraft_java_server)
|
||||
if (!address) return
|
||||
|
||||
if (options.instance.value) {
|
||||
try {
|
||||
await add_server_to_profile(
|
||||
options.instance.value.path,
|
||||
project.name,
|
||||
address,
|
||||
'prompt',
|
||||
project.project_id,
|
||||
project.minecraft_java_server?.content?.kind,
|
||||
)
|
||||
options.newlyInstalled.value.push(project.project_id)
|
||||
} catch (error) {
|
||||
options.handleError(error)
|
||||
}
|
||||
} else {
|
||||
options.showAddServerToInstanceModal(project.name, address)
|
||||
}
|
||||
}
|
||||
|
||||
async function pingServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('pingServerHits', { hitCount: hits.length })
|
||||
const pingsToFetch = hits.flatMap((hit) => {
|
||||
const address = hit.minecraft_java_server?.address
|
||||
if (!address) return []
|
||||
return [{ hit, address }]
|
||||
})
|
||||
const nextPings = { ...serverPings.value }
|
||||
for (const { hit, address } of pingsToFetch) {
|
||||
if (serverPingCache.has(address)) {
|
||||
nextPings[hit.project_id] = serverPingCache.get(address)
|
||||
}
|
||||
}
|
||||
serverPings.value = nextPings
|
||||
|
||||
await Promise.all(
|
||||
pingsToFetch.map(async ({ hit, address }) => {
|
||||
if (serverPingCache.has(address)) return
|
||||
|
||||
let pending = pendingServerPings.get(address)
|
||||
if (!pending) {
|
||||
pending = getServerLatency(address)
|
||||
.then((latency) => {
|
||||
if (serverPingCacheActive) serverPingCache.set(address, latency)
|
||||
return latency
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Failed to ping server ${address}:`, error)
|
||||
if (serverPingCacheActive) serverPingCache.set(address, undefined)
|
||||
return undefined
|
||||
})
|
||||
.finally(() => {
|
||||
pendingServerPings.delete(address)
|
||||
})
|
||||
pendingServerPings.set(address, pending)
|
||||
}
|
||||
|
||||
const latency = await pending
|
||||
if (!serverPingCacheActive) return
|
||||
serverPings.value = { ...serverPings.value, [hit.project_id]: latency }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function updateServerHits(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
lastServerHits.value = hits
|
||||
pingServerHits(hits)
|
||||
checkServerRunningStates(hits)
|
||||
}
|
||||
|
||||
function getServerModpackContent(project: Labrinth.Search.v3.ResultSearchProject) {
|
||||
const content = project.minecraft_java_server?.content
|
||||
if (content?.kind === 'modpack') {
|
||||
const { project_name, project_icon, project_id } = content
|
||||
if (!project_name) return undefined
|
||||
return {
|
||||
name: project_name,
|
||||
icon: project_icon ?? undefined,
|
||||
onclick:
|
||||
project_id !== project.project_id
|
||||
? () => {
|
||||
options.router.push(`/project/${project_id}`)
|
||||
}
|
||||
: undefined,
|
||||
showCustomModpackTooltip: project_id === project.project_id,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function getServerCardActions(
|
||||
serverResult: Labrinth.Search.v3.ResultSearchProject,
|
||||
): CardAction[] {
|
||||
const isInstalled = options.allInstalledIds.value.has(serverResult.project_id)
|
||||
|
||||
if (options.isFromWorlds.value && options.instance.value) {
|
||||
return [
|
||||
{
|
||||
key: 'add-to-instance',
|
||||
label: formatMessage(isInstalled ? messages.added : messages.addToInstance),
|
||||
icon: isInstalled ? CheckIcon : PlusIcon,
|
||||
disabled: isInstalled,
|
||||
color: 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => handleAddServerToInstance(serverResult),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const actions: CardAction[] = []
|
||||
|
||||
actions.push({
|
||||
key: 'add',
|
||||
label: '',
|
||||
icon: isInstalled ? CheckIcon : PlusIcon,
|
||||
disabled: isInstalled,
|
||||
circular: true,
|
||||
tooltip: isInstalled
|
||||
? formatMessage(messages.alreadyAdded)
|
||||
: options.instance.value
|
||||
? formatMessage(messages.addToInstanceName, {
|
||||
instanceName: options.instance.value.name,
|
||||
})
|
||||
: formatMessage(commonMessages.addServerToInstanceButton),
|
||||
onClick: () => handleAddServerToInstance(serverResult),
|
||||
})
|
||||
|
||||
if (runningServerProjects.value[serverResult.project_id]) {
|
||||
actions.push({
|
||||
key: 'stop',
|
||||
label: formatMessage(commonMessages.stopButton),
|
||||
icon: StopCircleIcon,
|
||||
color: 'red',
|
||||
type: 'outlined',
|
||||
onClick: () => handleStopServerProject(serverResult.project_id),
|
||||
})
|
||||
} else {
|
||||
const isInstalling = options.installingServerProjects.value.includes(serverResult.project_id)
|
||||
actions.push({
|
||||
key: 'play',
|
||||
label: formatMessage(
|
||||
isInstalling ? commonMessages.installingLabel : commonMessages.playButton,
|
||||
),
|
||||
icon: PlayIcon,
|
||||
disabled: isInstalling,
|
||||
color: 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => handlePlayServerProject(serverResult.project_id),
|
||||
})
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
function handleRightClick(
|
||||
event: MouseEvent,
|
||||
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
) {
|
||||
contextMenuRef.value?.showMenu(event, result, [{ name: 'open_link' }, { name: 'copy_link' }])
|
||||
}
|
||||
|
||||
function handleOptionsClick(args: ContextMenuOptionClick) {
|
||||
const url = getProjectUrl(args.item)
|
||||
switch (args.option) {
|
||||
case 'open_link':
|
||||
openUrl(url)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(url)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
process_listener((event: { event: string; profile_path_id: string }) => {
|
||||
debugLog('process event', event)
|
||||
if (event.event === 'finished') {
|
||||
const projectId = Object.entries(runningServerProjects.value).find(
|
||||
([, path]) => path === event.profile_path_id,
|
||||
)?.[0]
|
||||
if (projectId) {
|
||||
const { [projectId]: _, ...rest } = runningServerProjects.value
|
||||
runningServerProjects.value = rest
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
unlistenProcesses = unlisten
|
||||
})
|
||||
.catch(options.handleError)
|
||||
|
||||
onUnmounted(() => {
|
||||
serverPingCacheActive = false
|
||||
unlistenProcesses?.()
|
||||
serverPingCache.clear()
|
||||
pendingServerPings.clear()
|
||||
})
|
||||
|
||||
return {
|
||||
serverPings,
|
||||
contextMenuRef,
|
||||
updateServerHits,
|
||||
getServerModpackContent,
|
||||
getServerCardActions,
|
||||
handleRightClick,
|
||||
handleOptionsClick,
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectUrl(
|
||||
item: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
) {
|
||||
const projectType = 'project_types' in item ? item.project_types?.[0] : item.project_type
|
||||
return `https://modrinth.com/${projectType ?? 'project'}/${item.slug ?? item.project_id}`
|
||||
}
|
||||
@@ -36,18 +36,38 @@ export function useIntercomPositioning({
|
||||
() => route.path.startsWith('/browse') || route.path.startsWith('/project'),
|
||||
)
|
||||
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
||||
const intercomBubbleHorizontalPadding = computed(() =>
|
||||
const defaultIntercomBubbleHorizontalPadding = computed(() =>
|
||||
sidebarVisible.value
|
||||
? APP_SIDEBAR_WIDTH + INTERCOM_BUBBLE_DEFAULT_PADDING
|
||||
: INTERCOM_BUBBLE_DEFAULT_PADDING,
|
||||
)
|
||||
const intercomBubbleRequestedHorizontalPadding = ref<number | null>(null)
|
||||
const intercomBubbleHorizontalPadding = computed(
|
||||
() =>
|
||||
intercomBubbleRequestedHorizontalPadding.value ??
|
||||
defaultIntercomBubbleHorizontalPadding.value,
|
||||
)
|
||||
const intercomBubbleVerticalClearance = ref<number | null>(null)
|
||||
const intercomBubblePosition = computed(() => ({
|
||||
horizontalPadding: intercomBubbleHorizontalPadding.value,
|
||||
verticalPadding: intercomBubbleVerticalClearance.value ?? INTERCOM_BUBBLE_DEFAULT_PADDING,
|
||||
}))
|
||||
const intercomBubbleHorizontalPaddingRequests = new Map<symbol, number>()
|
||||
const intercomBubbleClearanceRequests = new Map<symbol, number>()
|
||||
|
||||
function requestIntercomBubbleHorizontalPadding(id: symbol, padding: number | null) {
|
||||
if (padding === null) {
|
||||
intercomBubbleHorizontalPaddingRequests.delete(id)
|
||||
} else {
|
||||
intercomBubbleHorizontalPaddingRequests.set(id, padding)
|
||||
}
|
||||
|
||||
intercomBubbleRequestedHorizontalPadding.value =
|
||||
intercomBubbleHorizontalPaddingRequests.size > 0
|
||||
? Math.max(...intercomBubbleHorizontalPaddingRequests.values())
|
||||
: null
|
||||
}
|
||||
|
||||
function requestIntercomBubbleVerticalClearance(id: symbol, clearance: number | null) {
|
||||
if (clearance === null) {
|
||||
intercomBubbleClearanceRequests.delete(id)
|
||||
@@ -93,6 +113,7 @@ export function useIntercomPositioning({
|
||||
intercomBubble: {
|
||||
width: ref(INTERCOM_BUBBLE_WIDTH),
|
||||
horizontalPadding: intercomBubbleHorizontalPadding,
|
||||
requestHorizontalPadding: requestIntercomBubbleHorizontalPadding,
|
||||
requestVerticalClearance: requestIntercomBubbleVerticalClearance,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -184,7 +184,7 @@ 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'
|
||||
export type DownloadReason = 'standalone' | 'dependency' | 'modpack' | 'update'
|
||||
|
||||
export async function add_project_from_version(
|
||||
path: string,
|
||||
|
||||
@@ -29,15 +29,12 @@
|
||||
"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.browse.server.installing": {
|
||||
"message": "جاري التثبيت"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "أدخل وصف التعديل..."
|
||||
},
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Prozkoumat servery"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Skrýt přidané servery"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Skrýt nainstalovaný obsah"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Nainstalovat obsah do instnce"
|
||||
},
|
||||
|
||||
@@ -20,12 +20,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Opdag servere"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Gem tilføjet servere"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Gem installeret indhold"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Indtast modpack beskrivelse..."
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java {version} wird heruntergeladen"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen ausblenden"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Zur primären Instanz machen"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Keine Instanzen laufen"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Primäre Instanz"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen anzeigen"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Instanz stoppen"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Aktive Downloads anzeigen"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Instanz anzeigen"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Protokolle anzeigen"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschärfeeffekte, welche ohne hardwarebeschleunigtes Rendering zu Leistungsproblemen führen können."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Erweitertes Rendering"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Wähle dein bevorzugtes Farbschema für die Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Farbschema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Ändere die Seite, die beim Öffnen des Launchers angezeigt wird."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Start"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Bibliothek"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Standard-Startseite"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Deaktiviert das Namensschild über deinem Spieler auf der Skin-Seite."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Namensschild ausblenden"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Enthält zuletzt gespielte Welten im Abschnitt „Direkt weiterspielen“ auf der Startseite."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Springe zurück in Welten"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Den Launcher minimieren, wenn ein Minecraft-Prozess gestartet wird."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Launcher minimieren"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Systemfensterrahmen verwenden (Neustart der App erforderlich)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Native Dekorationen"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Option wählen"
|
||||
},
|
||||
"app.appearance-settings.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."
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Server entdecken"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hinzugefügte Server ausblenden"
|
||||
"message": "Bereits hinzugefügte Server ausblenden"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Installierte Inhalte ausblenden"
|
||||
"message": "Bereits installierte Inhalte ausblenden"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Inhalt in Instanz installieren"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Installieren"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Installiert"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Wird installiert"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Modpack wird installiert..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Modpaketbeschreibung eingeben..."
|
||||
},
|
||||
@@ -575,6 +692,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Welt bereits in Benutzung"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Konto hinzufügen"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft-Konto"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Nicht angemeldet"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Konto entfernen"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Konto auswählen"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "In Minecraft anmelden"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Von der Instanz vorgegeben"
|
||||
},
|
||||
@@ -598,5 +733,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader wird vom Server bereitgestellt"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth hochgeladen wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Diese Warnung nicht mehr anzeigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Installation bestätigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Trotzdem installieren"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Schadsoftware wird häufig über Modpack-Dateien verbreitet, indem diese auf Plattformen wie Discord geteilt werden."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Wir konnten diese Datei auf Modrinth nicht finden. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Warnung vor unbekannter Datei"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java {version} wird heruntergeladen"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen ausblenden"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Zur primären Instanz machen"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Keine Instanzen laufen"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Primäre Instanz"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Weitere laufende Instanzen anzeigen"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Instanz stoppen"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Aktive Downloads anzeigen"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Instanz anzeigen"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Protokolle anzeigen"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Aktiviert erweiterte Rendering-Funktionen wie Unschä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.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."
|
||||
},
|
||||
@@ -6,7 +105,7 @@
|
||||
"message": "Authentifizierungsserver sind nicht erreichbar"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Server zu Instanz hinzufügen"
|
||||
"message": "Server zur Instanz hinzufügen"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Server zu deiner Instanz hinzufügen"
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Server entdecken"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hinzugefügte Server ausblenden"
|
||||
"message": "Bereits hinzugefügte Server ausblenden"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Installierte Inhalte ausblenden"
|
||||
"message": "Bereits installierte Inhalte ausblenden"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Inhalt in Instanz installieren"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Installieren"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Installiert"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Wird installiert"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Modpack wird installiert..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Beschreibung des Modpacks eingeben..."
|
||||
},
|
||||
@@ -575,6 +692,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Welt wird aktuell benutzt"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Konto hinzufügen"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft-Konto"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Nicht angemeldet"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Konto entfernen"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Konto auswählen"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Bei Minecraft anmelden"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Von der Instanz vorgegeben"
|
||||
},
|
||||
@@ -598,5 +733,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader vom Server vorgegeben"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Eine Datei wird nur geprüft, wenn sie auf Modrinth hochgeladen wird, unabhängig von ihrem Dateiformat (auch .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Diese Warnung nicht mehr anzeigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Installation bestätigen"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Trotzdem installieren"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Schadsoftware wird häufig über Modpack-Dateien verbreitet, indem diese auf Plattformen wie Discord geteilt werden."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Wir konnten diese Datei auf Modrinth nicht finden. Wir empfehlen dringend, nur Dateien aus vertrauenswürdigen Quellen zu installieren."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Warnung vor unbekannter Datei"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,11 +104,11 @@
|
||||
"app.auth-servers.unreachable.header": {
|
||||
"message": "Cannot reach authentication servers"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Add server to instance"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Add servers to your instance"
|
||||
"message": "Adding server to instance"
|
||||
},
|
||||
"app.browse.add-to-an-instance": {
|
||||
"message": "Add to an instance"
|
||||
},
|
||||
"app.browse.add-to-instance": {
|
||||
"message": "Add to instance"
|
||||
@@ -122,6 +122,9 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "Already added"
|
||||
},
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Back to instance"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Discover content"
|
||||
},
|
||||
@@ -131,20 +134,11 @@
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hide already added servers"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Hide already installed content"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Install"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Installed"
|
||||
"app.browse.server-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"
|
||||
@@ -164,6 +158,9 @@
|
||||
"app.export-modal.header": {
|
||||
"message": "Export modpack"
|
||||
},
|
||||
"app.export-modal.include-file-accessibility-label": {
|
||||
"message": "Include \"{file}\"?"
|
||||
},
|
||||
"app.export-modal.modpack-name-label": {
|
||||
"message": "Modpack Name"
|
||||
},
|
||||
@@ -171,7 +168,7 @@
|
||||
"message": "Modpack name"
|
||||
},
|
||||
"app.export-modal.select-files-label": {
|
||||
"message": "Select files and folders to include in pack"
|
||||
"message": "Configure which files are included in this export"
|
||||
},
|
||||
"app.export-modal.version-number-label": {
|
||||
"message": "Version number"
|
||||
@@ -302,6 +299,15 @@
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "An update is required to play {name}. Please update to the latest version to launch the game."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "This project is already installed"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Back to browse"
|
||||
},
|
||||
"app.project.install-context.install-content-to-instance": {
|
||||
"message": "Install content to instance"
|
||||
},
|
||||
"app.settings.developer-mode-enabled": {
|
||||
"message": "Developer mode enabled."
|
||||
},
|
||||
@@ -692,6 +698,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "World is in use"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Add account"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Minecraft account"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Not signed in"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Remove account"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Select account"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Sign in to Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Provided by the instance"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Descargando Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Descargas"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ocultar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Hacer instancia principal"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Ninguna instancia en ejecución"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Sin conexión"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instancia principal"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Mostrar mas instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Finalizar instancia"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Mostrar descargas activas"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Mostrar instancia"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Mostrar 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": "Seleccione su tema de color preferido para Modrinth en este dispositivo."
|
||||
},
|
||||
"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 se abre en."
|
||||
},
|
||||
"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 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"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Incluye los mundos más recientes en la sección \"Vuelve a jugar tus mundos\" en la página de inicio."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Vuelve a jugar tus mundos"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimiza el launcher cuando un proceso de Minecraft empieza."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimiza el launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa los sistemas de frame de Windows (se requiere resetear 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.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, te advertiremos de los riesgos 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."
|
||||
},
|
||||
@@ -30,7 +129,7 @@
|
||||
"message": "Descubrir servidores"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ocultar servidores añadidos"
|
||||
"message": "Ocultar servidores ya añadidos"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Ocultar contenido instalado"
|
||||
@@ -38,6 +137,24 @@
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instalar contenido a la instancia"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Instalar"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Instalado"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Instalando"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalando el modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Introduce la descripción del modpack..."
|
||||
},
|
||||
@@ -96,7 +213,7 @@
|
||||
"message": "Se añadieron {count} proyectos"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "¡Mira a los proyectos que estoy usando en mi modpack!"
|
||||
"message": "¡Mira los proyectos que estoy usando en mi modpack!"
|
||||
},
|
||||
"app.instance.mods.share-title": {
|
||||
"message": "Compartiendo contenido del modpack"
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "El loader es proporcionado por el servidor"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "No mostrar esta advertencia otra vez"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirmar instalación"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Instalar de todos modos"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "El malware es normalmente distribuido por archivos de modpack que son normalmente compartidas en aplicaciones como Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "No pudimos encontrar este archivo en Modrinth. Recomendamos solo instalar archivos de sitios de confianza."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Advertencia de archivo desconocido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Descargando Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Descargas"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ocultar más instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Hacer instancia principal"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Ninguna instancia en ejecución"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Sin conexión"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instancia principal"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Mostrar mas instancias en ejecución"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Finalizar instancia"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Mostrar descargas activas"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Mostrar instancia"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Mostrar 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": "Seleccione su tema de color preferido para Modrinth en este dispositivo."
|
||||
},
|
||||
"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 se abre en."
|
||||
},
|
||||
"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 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"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Incluye los mundos más recientes en la sección \"Vuelve a jugar tus mundos\" en la página de inicio."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Vuelve a jugar tus mundos"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimiza el launcher cuando un proceso de Minecraft empieza."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimiza el launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa los sistemas de frame de Windows (se requiere resetear 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.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, te advertiremos de los riesgos 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."
|
||||
},
|
||||
@@ -30,7 +129,7 @@
|
||||
"message": "Descubrir servidores"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ocultar servidores añadidos"
|
||||
"message": "Ocultar servidores ya añadidos"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Ocultar contenido instalado"
|
||||
@@ -38,6 +137,24 @@
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instalar contenido a una instancia"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Instalar"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Instalado"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Instalando"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalando el modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Escribe la descripción del modpack..."
|
||||
},
|
||||
@@ -165,7 +282,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"
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "El cargador lo proporciona el servidor"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Un archivo solo es revisado si es subido a Modrinth, independientemente de su formato de archivo (incluyendo .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "No mostrar esta advertencia otra vez"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirmar instalación"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Instalar de todos modos"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "El malware es normalmente distribuido por archivos de modpack que son normalmente compartidas en aplicaciones como Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "No pudimos encontrar este archivo en Modrinth. Recomendamos solo instalar archivos de sitios de confianza."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Advertencia de archivo desconocido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Löydä palvelimia"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Piilota lisätyt palvelimet"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Piilota asennettu sisältö"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Lataa sisältöä instanssiin"
|
||||
},
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Tumuklas ng mga server"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Taguin ang mga nadagdag na server"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Taguin ang mga na-install na kontento"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "I-install ang kontento sa instansiya"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java version {version} en cours de téléchargement"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Téléchargements"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Masquer plus d'instances en exécution"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Définir en tante qu'instance première"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Aucune instance en exécution"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Hors ligne"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instance première"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Montrer plus d'instances en exécution"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Arrêter l'instance"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Voir les téléchargements actifs"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Voir l'instance"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Voir les journaux"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Permet un rendu avancé tel que des effets de flou qui peuvent causer des problèmes de performance sans rendu accéléré par le matériel."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Rendu avancé"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Sélectionnez votre couleur préférée pour le thème de Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Thème couleur"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Modifier la page sur laquelle le lancheur s'ouvre."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Acceuil"
|
||||
},
|
||||
"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 launcher quand Minecraft démarre."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimiser le launcher"
|
||||
},
|
||||
"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.toggle-sidebar.description": {
|
||||
"message": "Permet d'ouvrir de 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."
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Découvrir des serveurs"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Masquer les serveurs ajoutés"
|
||||
"message": "Masquer les serveurs déjà ajoutés"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Masquer le contenu installé"
|
||||
"message": "Masquer le contenu déjà ajouté"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Installer du contenu à l'instance"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpacks"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Installer"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Installé"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installation en cours"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installation du modpack..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Saisir la description du modpack..."
|
||||
},
|
||||
@@ -575,6 +692,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Le monde en cours d'utilisation"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Ajouter un compte"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Compte Minecraft"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Pas connecté"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Retirer le compte"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Sélectionner un compte"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Se connecter à Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Procuré par l'instance"
|
||||
},
|
||||
@@ -598,5 +733,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Le loader est procuré par le serveur"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Un fichier n’est révisé que s’il est téléchargé sur Modrinth, quel que soit son format de fichier (y compris .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Ne plus m'avertir à ce sujet"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirmer l'installation"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Installer tout de même"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Les logiciels malveillants sont souvent distribués via des fichiers modpack en les partageant sur des plateformes comme Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Nous ne pouvions pas trouver ce fichier sur Modrinth. Nous vous recommandons fortement de n'installer que des fichiers de sources de confiance."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Avertissement fichier inconnu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "גלה שרתים"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "הסתר שרתים שנוספו"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "הסתר תוכן מותקן"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "הוספת התוכן להתקנה"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "A Java {version} 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": "Elsődleges példány létrehozása"
|
||||
},
|
||||
"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.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."
|
||||
},
|
||||
@@ -30,14 +51,26 @@
|
||||
"message": "Szerverek böngészése"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Hozzáadott szerverek elrejtése"
|
||||
"message": "Már hozzáadott szerverek elrejtése"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "A telepített tartalmak elrejtése"
|
||||
"message": "Már telepített tartalom elrejtése"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Tartalom letöltése a profilhoz"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modcsomagok"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Letöltés"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Letöltve"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Letöltés..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Írd be a modcsomag leírását..."
|
||||
},
|
||||
@@ -210,10 +243,10 @@
|
||||
"message": "Erőforráskezelés"
|
||||
},
|
||||
"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."
|
||||
"message": "A Modrinth App v{version} telepítésre kész! Frissítéshez válaszd ki a Frissítés opciót, vagy az alkalmazás 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 Töltsd újra az oldalt, vagy a Modrinth App bezárásakor automatikusan frissül."
|
||||
"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 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!"
|
||||
@@ -231,7 +264,7 @@
|
||||
"message": "Sikeres letöltés"
|
||||
},
|
||||
"app.update-popup.reload": {
|
||||
"message": "Újratöltés"
|
||||
"message": "Frissítés"
|
||||
},
|
||||
"app.update-popup.title": {
|
||||
"message": "Frissítés elérhető"
|
||||
@@ -555,7 +588,7 @@
|
||||
"message": "A szerver nem kompatibilis"
|
||||
},
|
||||
"instance.worlds.linked_server": {
|
||||
"message": "Szerverprojekt által kezelt"
|
||||
"message": "A szerver projektje által kezelt"
|
||||
},
|
||||
"instance.worlds.no_contact": {
|
||||
"message": "A szerverrel nem lehet kapcsolatot létesíteni"
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Mengunduh Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Mengunduh"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Sembunyikan lebih banyak instance yang sedang berjalan"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Jadikan sebagai instance utama"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Tidak ada instance yang sedang berjalan"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instance utama"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Tampilkan lebih banyak instance yang sedang berjalan"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Server autentikasi Minecraft mungkin sedang tidak tersedia saat ini. Periksa koneksi internet Anda dan coba lagi nanti."
|
||||
},
|
||||
@@ -29,12 +53,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Temukan server"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Sembuyikan server tertambah"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Sembunyikan konten terpasang"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Pasang konten ke instans"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Scaricando Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Download"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Nascondi le 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.show-more-running-instances": {
|
||||
"message": "Mostra le istanze in esecuzione"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Ferma l'istanza"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Vedi i download attivi"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Vedi l'istanza"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Vedi 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 nametag"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Aggiunge 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 quanto si avvia Minecraft."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Riduci il launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usa la cornice di sistema (riavvio richiesto)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Decorazioni native"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Seleziona un'opzione"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Scegli se mostrare o nascondere la barra laterale."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Abilita la barra laterale"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Apparirà un avviso prima di installare un file Modrinth Pack (.mrpack) non proveniente da Modrinth, così da conoscerne i rischi."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Avvisami prima di installare 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."
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Esplora i server"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Nascondi server aggiunti"
|
||||
"message": "Nascondi server già aggiunti"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Nascondi contenuti installati"
|
||||
"message": "Nascondi contenuti già installati"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Installa contenuti nell'istanza"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Pacchetti di mod"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Installa"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Installato"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installando"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Installando il pacchetto..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Inserisci descrizione del pacchetto..."
|
||||
},
|
||||
@@ -575,6 +692,24 @@
|
||||
"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"
|
||||
},
|
||||
@@ -598,5 +733,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Il loader è determinato dal server"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Un file è esaminato quando è caricato su Modrinth, indipendentemente dal suo 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": "Molto spesso i malware sono distribuiti attraverso i pacchetti di mod, distribuiti in piattaforme come Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Non siamo riusciti a trovare questo file su Modrinth. Consigliamo di installare file solo da fonti di cui ti fidi."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Tipo di file sconosciuto"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java {version} をダウンロード中"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "ダウンロード"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "実行中のインスタンスの一部を非表示"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "デフォルトのインスタンスを作成"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "実行中のインスタンスはありません"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "オフライン"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "デフォルトのインスタンス"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "実行中のインスタンスをさらに表示"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "インスタンスを停止"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "進行中のダウンロードを表示"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "インスタンスを表示"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "ログを表示"
|
||||
},
|
||||
"app.appearance-settings.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.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の認証サーバーは現在停止している可能性があります。インターネット接続を確認し、しばらくしてからもう一度お試しください。"
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "サーバーを探す"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "追加済みのサーバーを隠す"
|
||||
"message": "既に追加済みのサーバーを非表示にする"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "インストール済みのコンテンツを隠す"
|
||||
"message": "既にインストールされているコンテンツを非表示にする"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "コンテンツをインスタンスにインストールする"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modパック"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "インストール"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "インストール済み"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "インストール中"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Modパックをインストール中..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Modパックの説明を入力…"
|
||||
},
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"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": "不明なファイルの警告"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java {version} 다운로드 중"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "다운로드"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "실행 중인 인스턴스 숨기기"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "기본 인스턴스로 지정"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "실행 중인 인스턴스 없음"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "오프라인"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "기본 인스턴스"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "실행 중인 인스턴스 보이기"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "인스턴스 중지"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "다운로드 목록 보기"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "인스턴스 보기"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "로그 보기"
|
||||
},
|
||||
"app.appearance-settings.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": "마인크래프트 실행 시 런처를 최소화합니다."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "런처 최소화"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "시스템 창 테두리 (앱 재시작 필요)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "네이티브 제목 표시줄"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "옵션 선택"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "사이드바를 접거나 펼칠 수 있는 기능을 활성화합니다."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "사이드바 토글"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Modrinth 외부에서 가져온 모드팩 파일(.mrpack)을 설치할 때, 사용자가 위험성을 인지할 수 있도록 경고를 표시합니다."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "출처를 알 수 없는 모드팩 설치 전 경고"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft 인증 서버가 일시적으로 중단되었을 수 있습니다. 인터넷 연결을 확인한 후 나중에 다시 시도하세요."
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "서버 탐색하기"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "추가된 서버 숨기기"
|
||||
"message": "이미 추가된 서버 숨기기"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "설치된 콘텐츠 숨기기"
|
||||
"message": "이미 설치된 콘텐츠 숨기기"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "인스턴스에 콘텐츠 설치"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "모드팩"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "설치"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "설치됨"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "설치 중"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "모드팩 설치중..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "모드팩 설명 입력..."
|
||||
},
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "로더가 서버에 의해 제공됩니다"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "모든 파일은 형식(.mrpack 포함)에 무관하게 Modrinth에 업로드되어야만 검수를 거칩니다."
|
||||
},
|
||||
"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": "악성코드는 흔히 디스코드와 같은 플랫폼을 통해 모드팩 파일을 공유하는 방식으로 유포됩니다."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "이 파일을 Modrinth에서 찾을 수 없습니다. 신뢰할 수 있는 출처의 파일만 설치하는 것을 권장합니다."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "출처를 알 수 없는 파일 경고"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Sedang memuat turun Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Muat Turun"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Laman utama"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Pelayan pengesahan Minecraft mungkin sedang tergendala sekarang. Periksa sambungan internet anda dan cuba lagi kemudian."
|
||||
},
|
||||
@@ -29,12 +38,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Temui pelayan"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Sembunyikan pelayan yang ditambah"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Sembunyikan kandungan yang dipasang"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Pasang kandungan ke dalam pemasangan"
|
||||
},
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Ontdek servers"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Verstop toegevoegde servers"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Verberg Geïnstalleerde inhoud"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Installeer inhoud naar instantie"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Pobieranie Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Pobrania"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ukryj włączone instancje"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Ustaw jako główną instancję"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Żadna instancja nie jest włączona"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Główna instancja"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Pokaż więcej włączonych instancji"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Zatrzymaj instancję"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Pokaż aktualnie pobierane"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Pokaż instancję"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Pokaż logi"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Włącza zaawansowane renderowanie, takie jak efekty rozmycia, które może powodować problemy z wydajnością bez sprzętowej akceleracji renderowania."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Zaawansowane renderowanie"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Wybierz preferowany motyw w Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Motyw"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Ustaw stronę, która pojawia się po włączeniu launchera."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Strona główna"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Biblioteka"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Domyślna strona główna"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Wyłącza nazwę użytkownika na stronie skórek."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Schowaj nazwę"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Pokazuje ostatnie światy w sekcji \"Wskocz z powrotem do światów\" na stronie głównej."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Wskocz z powrotem do światów"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimalizuj launcher po włączeniu procesu Minecraft."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimalizuj launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Używaj systemowej ramki okna (wymaga ponownego uruchomienia)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Systemowe dekoracje"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Wybierz opcję"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Umożliwia przełączenie paska bocznego."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Przełącz pasek boczny"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Jeżeli spróbujesz zainstalować plik Modrinth Pack (.mrpack) którego nie można znaleźć na Modrinth, poinformujemy cię o możliwym ryzyku, zanim go zainstalujesz."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Ostrzegaj mnie przed instalowaniem nieznanych paczek modów"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Serwery uwierzytelniania Minecraft mogą aktualnie nie działać. Sprawdź swoje połączenie z internetem i spróbuj ponownie później."
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Odkryj serwery"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ukryj dodane serwery"
|
||||
"message": "Ukryj już dodane serwery"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Ukryj zainstalowane zasoby"
|
||||
"message": "Ukryj już dodane zasoby"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Zainstaluj zasoby do instancji"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Paczki modów"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Zainstaluj"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Zainstalowano"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Instalowanie"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalowanie paczki modów..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Wprowadź opis paczki modów..."
|
||||
},
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Loader jest dostarczony przez serwer"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Plik jest sprawdzony tylko, jeżeli został przesłany na Modrinth, niezależnie od formatu (w tym pliki .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Nie pokazuj ponownie"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Potwierdź instalację"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Instaluj mimo to"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Wirusy są często kryte w plikach paczek modów przesyłanych na platformach takich jak Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Nie mogliśmy znaleźć tego pliku na Modrinth. Stanowczo zalecamy instalowanie plików tylko ze źródeł, którym ufasz."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Ostrzeżenie o nieznanym pliku"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Baixando java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Downloads"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Ocultar mais instâncias em execução"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Criar instância primária"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Nenhuma instância em execução"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Offline"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Instância primária"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Mostrar mais instâncias em execução"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Parar instância"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Ver downloads ativos"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Ver instância"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Ver logs"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Ativa a renderização avançada, como efeitos de desfoque que podem causar problemas de desempenho sem a renderização acelerada de hardware."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Renderização avançada"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Selecione seu tema de cores preferido para o Modrinth App."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema de Cores"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Alterar a página em que o launcher é aberto."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Início"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Biblioteca"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Página inicial padrão"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Desativa o nome acima do seu jogador na página de skins."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "Ocultar nome"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Inclui mundos recentes na seção \"Voltar à ação\" na página inicial."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Volte aos mundos"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minimize o launcher quando um processo do Minecraft for iniciado."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimizar launcher"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Usar molduras de janela do sistema (requer reiniciar o aplicativo)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Decorações nativas"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Selecione uma opção"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Ativa a capacidade de alternar a visibilidade da barra lateral."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Alternar barra lateral"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Se você tentar instalar um arquivo Modrinth Pack (.mrpack) que não esteja hospedado no Modrinth, garantiremos que você entenda os riscos antes de instalá-lo."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Avise-me antes de instalar pacotes de mods desconhecidos"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Os servidores de autenticação do Minecraft podem estar indisponíveis no momento. Verifique sua conexão com a rede e tente novamente mais tarde."
|
||||
},
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Descubra servidores"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ocultar servidores adicionados"
|
||||
"message": "Ocultar servidores já adicionados"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Ocultar conteúdo instalado"
|
||||
"message": "Ocultar conteúdo já adicionado"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instalar conteúdo na instância"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Pacotes de mods"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Instalar"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Instalado"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Instalando"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Instalando pacote de mods..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Insira uma descrição para o pacote de mods..."
|
||||
},
|
||||
@@ -564,7 +681,7 @@
|
||||
"message": "Você só pode entrar diretamente em servidores a partir do Minecraft 1.0.5"
|
||||
},
|
||||
"instance.worlds.no_singleplayer_quick_play": {
|
||||
"message": "Você só pode entrar diretamente em mundos de um jogador a partir do Minecraft 1.20"
|
||||
"message": "Você só pode entrar diretamente em mundos de um jogador a partir do Minecraft 1.20+"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "Jogar na instância"
|
||||
@@ -575,6 +692,24 @@
|
||||
"instance.worlds.world_in_use": {
|
||||
"message": "Mundo em uso"
|
||||
},
|
||||
"minecraft-account.add-account": {
|
||||
"message": "Adicionar conta"
|
||||
},
|
||||
"minecraft-account.label": {
|
||||
"message": "Conta do minecraft"
|
||||
},
|
||||
"minecraft-account.not-signed-in": {
|
||||
"message": "Não conectado"
|
||||
},
|
||||
"minecraft-account.remove-account": {
|
||||
"message": "Remover conta"
|
||||
},
|
||||
"minecraft-account.select-account": {
|
||||
"message": "Selecionar conta"
|
||||
},
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Faça login no Minecraft"
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Fornecido pela instância"
|
||||
},
|
||||
@@ -598,5 +733,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "O carregador é fornecido pelo servidor"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Um arquivo só é revisado se for enviado para o Modrinth, não importa o seu formato de arquivo (incluindo .mrpack)."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Não mostrar este aviso novamente"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "Confirmar instalação"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Instalar mesmo assim"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Malware é frequentemente distribuído através de arquivos de pacotes de mods compartilhados em plataformas como o Discord."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Não foi possível encontrar este arquivo no Modrinth. Recomendamos fortemente que você instale apenas arquivos de fontes confiáveis."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Aviso de arquivo desconhecido"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Descobrir servidores"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Esconder servidores adicionados"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Esconder conteúdo instalado"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instalar conteúdo à instância"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Скачивание Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Скачивания"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Скрыть другие активные сборки"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Сделать основной сборкой"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Нет активных сборок"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Офлайн"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Основная сборка"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Показать другие активные сборки"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Остановить сборку"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Посмотреть текущие скачивания"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Посмотреть сборку"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Посмотреть журнал"
|
||||
},
|
||||
"app.appearance-settings.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": "Показывать недавние миры в разделе «Jump back in» на главной."
|
||||
},
|
||||
"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": "Использовать системную рамку окна (требуется перезапуск)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Системное оформление"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Выберите вариант"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Показывать кнопку скрытия боковой панели."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Переключатель боковой панели"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Предупреждать о рисках перед установкой файлов .mrpack, не размещённых на Modrinth."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Предупреждение о сторонних сборках"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Серверы аутентификации Minecraft сейчас могут быть недоступны. Проверьте подключение к интернету и повторите попытку позже."
|
||||
},
|
||||
@@ -38,6 +137,24 @@
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Установка контента в сборку"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Сборки"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Установить"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Установлено"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Установка"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Установка сборки..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Введите описание сборки..."
|
||||
},
|
||||
@@ -90,10 +207,10 @@
|
||||
"message": "проект"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "\"{name}\" был добавлен"
|
||||
"message": "«{name}» добавлен"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "{count} Проектов было добавлено"
|
||||
"message": "{count, plural, one {Добавлен # проект} few {Добавлено # проекта} other {Добавлено # проектов}}"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "Что в моей сборке:"
|
||||
@@ -369,7 +486,7 @@
|
||||
"message": "Настройка мира"
|
||||
},
|
||||
"instance.files.adding-files": {
|
||||
"message": "Добавление файлов ({completed}/{total})"
|
||||
"message": "Добавление файлов ({completed, number}/{total, number})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "Сохранить как..."
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Этот файл не найден на Modrinth. Рекомендуется скачивать файлы только из надёжных источников."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Предупреждение о неизвестном файле"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Otkrij servere"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Sakrije dodate servere"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Sakrije instalirani sadržaj"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Instaliraj sadržaj na instancu"
|
||||
},
|
||||
|
||||
@@ -29,15 +29,21 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Upptäck servrar"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Dölj tillagda servrar"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Dölj installerat innehåll"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Installera innehåll till instans"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Modpaket"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Installera"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Installerad"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Installerar"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Ange modpaketets beskrivning..."
|
||||
},
|
||||
|
||||
@@ -23,12 +23,6 @@
|
||||
"app.browse.already-added": {
|
||||
"message": "เพิ่มแล้ว"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "ซ่อนเซิร์ฟเวอร์ที่เพิ่มแล้ว"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "ซ่อนเนื้อหาที่ติดตั้งแล้ว"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "ติดตั้งเนื้อหาลงในอินสแตนซ์"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Java {version} indiriliyor"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "İndirmeler"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Diğer çalışan kurulumları gizle"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Birincil kurulum yap"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Herhangi bir kurulum çalışmıyor"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Çevrimdışı"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Birincil kurulum"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Daha fazla çalışan kurulum göster"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Kurulumu durdur"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Şu anda indirilienleri göster"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Kurulumu göster"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Günlükleri görüntüle"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Donanım hızlandırmalı işleme kapalıyken, performans sorunlarına yol açabilecek bulanıklık efektleri gibi gelişmiş işlemeleri aktif eder."
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.title": {
|
||||
"message": "Gelişmiş işleme"
|
||||
},
|
||||
"app.appearance-settings.color-theme.description": {
|
||||
"message": "Modrinth App için tercih ettiğiniz temayı seçin."
|
||||
},
|
||||
"app.appearance-settings.color-theme.title": {
|
||||
"message": "Tema"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.description": {
|
||||
"message": "Launcher açıldığında varsayılan olarak açılacak sayfayı seçin."
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.home": {
|
||||
"message": "Ana Sayfa"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.library": {
|
||||
"message": "Kütüphane"
|
||||
},
|
||||
"app.appearance-settings.default-landing-page.title": {
|
||||
"message": "Varsayılan karşılama sayfası"
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.description": {
|
||||
"message": "Kostümler sayfasındaki karakterinizin üzerindeki isim etiketini devre dışı bırakır."
|
||||
},
|
||||
"app.appearance-settings.hide-nametag.title": {
|
||||
"message": "İsim etiketini gizle"
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.description": {
|
||||
"message": "Ana sayfadaki 'Hızlıca Geri Dön' bölümünde son oynanan dünyaları gösterir."
|
||||
},
|
||||
"app.appearance-settings.jump-back-into-worlds.title": {
|
||||
"message": "Dünyalara hızlıca geri dön"
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.description": {
|
||||
"message": "Minecraft başlatıldığında launcher'i simge durumuna at."
|
||||
},
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Launcher'i simge durumuna at"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.description": {
|
||||
"message": "Sistem pencere kenarlıklarını kullan (Yeniden başlatma gerekli)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Yerel pencere öğeleri"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Bir seçenek seçin"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Kenar çubuğunu ayarlama özelliğini aktifleştirir."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Kenar çubuğunu ayarla"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Modrinth üzerinde barındırılmayan bir Modrinth Paket dosyasını (.mrpack) yüklemeye çalışırsanız, yükleme öncesinde riskleri anladığınızdan emin oluruz."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Bilinmeyen mod paketlerini indirmeden önce beni uyar"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft doğrulama sunucuları kapalı olabilir. İnternet bağlantını kontrol et ve daha sonra tekrar dene."
|
||||
},
|
||||
@@ -6,7 +105,7 @@
|
||||
"message": "Doğrulama sunucularına erişilemedi"
|
||||
},
|
||||
"app.browse.add-server-to-instance": {
|
||||
"message": "Kurulumunuza sunucu ekleyin"
|
||||
"message": "Sunucuyu kurulumu ekle"
|
||||
},
|
||||
"app.browse.add-servers-to-instance": {
|
||||
"message": "Kurulumunuza sunucular ekleyin"
|
||||
@@ -30,14 +129,32 @@
|
||||
"message": "Sunucu keşfet"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ekli sunucuları gizle"
|
||||
"message": "Zaten eklenmiş sunucuları gizle"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Kurulu içerikleri gizle"
|
||||
"message": "Zaten yüklenmiş içerikleri gizle"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "İçeriği kuruluma yükle"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Mod paketleri"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Yükle"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Yüklü"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Yükleniyor"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Mod paketi indiriliyor..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Mod paketi açıklaması girin..."
|
||||
},
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"search.filter.locked.server-loader.title": {
|
||||
"message": "Yükleyici sunucu tarafından sağlanıyor"
|
||||
},
|
||||
"unknown-pack-warning-modal.body": {
|
||||
"message": "Dosya formatı ne olursa olsun (.mrpack dahil), bir dosya yalnızca Modrinth'e yüklendiğinde denetlenir."
|
||||
},
|
||||
"unknown-pack-warning-modal.dont-show-again": {
|
||||
"message": "Bu uyarıyı tekrar gösterme"
|
||||
},
|
||||
"unknown-pack-warning-modal.header": {
|
||||
"message": "İndirmeyi onayla"
|
||||
},
|
||||
"unknown-pack-warning-modal.install-anyway": {
|
||||
"message": "Yine de indir"
|
||||
},
|
||||
"unknown-pack-warning-modal.malware-statement": {
|
||||
"message": "Kötü amaçlı yazılımlar genellikle Discord gibi platformlarda paylaşılan mod paketi dosyaları aracılığıyla yayılır."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Bu dosyayı Modrinth üzerinde bulamadık. Yalnızca güvendiğiniz kaynaklardan gelen dosyaları yüklemenizi şiddetle öneririz."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Bilinmeyen dosya uyarısı"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "Завантаження Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "Завантаження"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Приховати більше запущених екземплярів"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Зробити основним екземпляром"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "Немає запущених екземплярів"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "Офлайн"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "Основний екземпляр"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "Показати більше запущених екземплярів"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "Зупинити екземпляр"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "Переглянути активні завантаження"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "Переглянути екземпляр"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Переглянути журнали"
|
||||
},
|
||||
"app.appearance-settings.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": "Використовувати рамку системного вікна (потрібно перезапустити програму)."
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Рідні прикраси"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Виберіть опцію"
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.description": {
|
||||
"message": "Вмикає можливість перемикання бічної панелі."
|
||||
},
|
||||
"app.appearance-settings.toggle-sidebar.title": {
|
||||
"message": "Перемикання бічної панелі"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.description": {
|
||||
"message": "Якщо ви спробуєте встановити файл пакета Modrinth (.mrpack), який не розміщено на Modrinth, ми переконаємося, що ви розумієте ризики, перш ніж встановлювати його."
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "Попереджати мене перед встановленням невідомих модпаків"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Сервери автентифікації Minecraft можуть зараз не працювати. Перевірте з’єднання з інтернетом та спробуйте пізніше."
|
||||
},
|
||||
@@ -30,16 +129,34 @@
|
||||
"message": "Дослідити сервера"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Сховати додані сервери"
|
||||
"message": "Скрити наявні сервери"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Сховати встановлений уміст"
|
||||
"message": "Скрити доданий контент"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Установити вміст в профіль"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "Модпаки"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "Установлено"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "Встановлено"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "Встановлення"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "Встановлення модпаку..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "Уведіть опис збірки…"
|
||||
"message": "Уведіть опис збірки..."
|
||||
},
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Експортувати"
|
||||
@@ -144,7 +261,7 @@
|
||||
"message": "Ви впевнені, що хочете видалити «{name}»?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Пошук {count} світів…"
|
||||
"message": "Пошук {count} світів..."
|
||||
},
|
||||
"app.instance.worlds.this-server": {
|
||||
"message": "цей сервер"
|
||||
@@ -324,7 +441,7 @@
|
||||
"message": "Немає друзів, які збігаються з «{query}»"
|
||||
},
|
||||
"friends.search-friends-placeholder": {
|
||||
"message": "Пошук друзів…"
|
||||
"message": "Пошук друзів..."
|
||||
},
|
||||
"friends.section.heading": {
|
||||
"message": "{title} — {count}"
|
||||
@@ -372,7 +489,7 @@
|
||||
"message": "Додавання файлів ({completed}/{total})"
|
||||
},
|
||||
"instance.files.save-as": {
|
||||
"message": "Зберегти як…"
|
||||
"message": "Зберегти як..."
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Адреса"
|
||||
@@ -598,5 +715,26 @@
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "Ми не змогли знайти цей файл на Modrinth. Ми рекомендуємо встановлювати файли лише з тих джерел яким ви довіряєте."
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "Попередження про невідомий файл"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Khám phá máy chủ"
|
||||
},
|
||||
"app.browse.hide-added-servers": {
|
||||
"message": "Ẩn các máy chủ đã thêm"
|
||||
},
|
||||
"app.browse.hide-installed-content": {
|
||||
"message": "Ẩn các nội dung đã được tải xuống"
|
||||
},
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "Tải nội dung này vào hồ sơ"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "正在下载 Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "下载"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "隐藏更多运行中的实例"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "设为主实例"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "没有实例正在运行"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "离线"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "主实例"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "显示更多运行中的实例"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "停止实例"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "查看正在下载内容"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "查看实例"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "查看日志"
|
||||
},
|
||||
"app.appearance-settings.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": "使用系统窗口边框(需要重启应用)。"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "原生窗口"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"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 打包文件(.mrpack),我们会确保你在安装之前了解相关风险。"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "安装未知整合包前提醒我"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft 身份验证服务器现在可能无法使用。请检查你的网络连接,并稍后再试。"
|
||||
},
|
||||
@@ -38,6 +137,24 @@
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "将内容安装到实例中"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "整合包"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "安装"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "已安装"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "安装中"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "正在安装整合包……"
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "输入整合包描述……"
|
||||
},
|
||||
@@ -575,6 +692,24 @@
|
||||
"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": "由该实例提供"
|
||||
},
|
||||
@@ -598,5 +733,26 @@
|
||||
},
|
||||
"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 等平台上分享而传播。"
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "我们在 Modrinth 上找不到此文件。强烈建议你仅从可信来源安装文件。"
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "未知文件警告"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
{
|
||||
"app.action-bar.downloading-java": {
|
||||
"message": "正在下載 Java {version}"
|
||||
},
|
||||
"app.action-bar.downloads": {
|
||||
"message": "下載"
|
||||
},
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "隱藏更多執行中的實例"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "設為主要實例"
|
||||
},
|
||||
"app.action-bar.no-instances-running": {
|
||||
"message": "沒有執行中的實例"
|
||||
},
|
||||
"app.action-bar.offline": {
|
||||
"message": "離線"
|
||||
},
|
||||
"app.action-bar.primary-instance": {
|
||||
"message": "主要實例"
|
||||
},
|
||||
"app.action-bar.show-more-running-instances": {
|
||||
"message": "顯示更多執行中的實例"
|
||||
},
|
||||
"app.action-bar.stop-instance": {
|
||||
"message": "停止實例"
|
||||
},
|
||||
"app.action-bar.view-active-downloads": {
|
||||
"message": "查看正在下載的項目"
|
||||
},
|
||||
"app.action-bar.view-instance": {
|
||||
"message": "查看實例"
|
||||
},
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "查看紀錄檔"
|
||||
},
|
||||
"app.appearance-settings.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": "使用系統視窗外框(需要重新啟動應用程式)。"
|
||||
},
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "原生裝飾"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"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 封裝檔案 (.mrpack),我們會在安裝前確保你已了解相關風險。"
|
||||
},
|
||||
"app.appearance-settings.unknown-pack-warning.title": {
|
||||
"message": "在安裝未知模組包前發出警告"
|
||||
},
|
||||
"app.auth-servers.unreachable.body": {
|
||||
"message": "Minecraft 驗證伺服器現在可能無法使用。請檢查網際網路連線,然後再試一次。"
|
||||
},
|
||||
@@ -38,6 +137,24 @@
|
||||
"app.browse.install-content-to-instance": {
|
||||
"message": "安裝至實例中"
|
||||
},
|
||||
"app.browse.project-type.modpacks": {
|
||||
"message": "模組包"
|
||||
},
|
||||
"app.browse.server.install": {
|
||||
"message": "安裝"
|
||||
},
|
||||
"app.browse.server.installed": {
|
||||
"message": "已安裝"
|
||||
},
|
||||
"app.browse.server.installing": {
|
||||
"message": "安裝中"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.description": {
|
||||
"message": "{fileName}"
|
||||
},
|
||||
"app.creation-modal.installing-modpack.title": {
|
||||
"message": "正在安裝模組包..."
|
||||
},
|
||||
"app.export-modal.description-placeholder": {
|
||||
"message": "輸入模組包描述..."
|
||||
},
|
||||
@@ -75,7 +192,7 @@
|
||||
"message": "刪除實例"
|
||||
},
|
||||
"app.instance.modpack-already-installed.body": {
|
||||
"message": "此模組包已經安裝在<bold>{instanceName}</bold> 實例中了,您確定要再次安裝嗎?"
|
||||
"message": "這個模組包已經安裝在實例「<bold>{instanceName}</bold>」中了,確定要再次安裝嗎?"
|
||||
},
|
||||
"app.instance.modpack-already-installed.create": {
|
||||
"message": "建立"
|
||||
@@ -90,10 +207,10 @@
|
||||
"message": "專案"
|
||||
},
|
||||
"app.instance.mods.project-was-added": {
|
||||
"message": "新增了「{name}」"
|
||||
"message": "已新增「{name}」"
|
||||
},
|
||||
"app.instance.mods.projects-were-added": {
|
||||
"message": "新增了 {count} 個專案"
|
||||
"message": "已新增 {count} 個專案"
|
||||
},
|
||||
"app.instance.mods.share-text": {
|
||||
"message": "快來看看我在模組包中使用的專案!"
|
||||
@@ -102,7 +219,7 @@
|
||||
"message": "分享模組包內容"
|
||||
},
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "上傳成功"
|
||||
"message": "已成功上傳"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "新增伺服器"
|
||||
@@ -111,10 +228,10 @@
|
||||
"message": "瀏覽伺服器"
|
||||
},
|
||||
"app.instance.worlds.delete-world-description": {
|
||||
"message": "「{name}」將會被**永久刪除**,且無法還原。"
|
||||
"message": "「{name}」將**永久刪除**,且無法還原。"
|
||||
},
|
||||
"app.instance.worlds.delete-world-title": {
|
||||
"message": "你確定要永久刪除這個世界嗎?"
|
||||
"message": "確定要永久刪除這個世界嗎?"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "模組"
|
||||
@@ -141,7 +258,7 @@
|
||||
"message": "「{name}」({address}) 將從你的清單中移除(包含遊戲內),且無法還原。"
|
||||
},
|
||||
"app.instance.worlds.remove-server-title": {
|
||||
"message": "你確定要移除「{name}」嗎?"
|
||||
"message": "確定要移除「{name}」嗎?"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "搜尋 {count} 個世界..."
|
||||
@@ -321,7 +438,7 @@
|
||||
"message": "待處理"
|
||||
},
|
||||
"friends.no-friends-match": {
|
||||
"message": "沒有符合「{query}」的好友"
|
||||
"message": "找不到相符「{query}」的好友"
|
||||
},
|
||||
"friends.search-friends-placeholder": {
|
||||
"message": "搜尋好友..."
|
||||
@@ -426,13 +543,13 @@
|
||||
"message": "選擇圖示"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups": {
|
||||
"message": "實例庫群組"
|
||||
"message": "遊戲庫群組"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.create": {
|
||||
"message": "建立新的群組"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.description": {
|
||||
"message": "實例庫群組讓你可以將實例整理到實例庫中的不同分類。"
|
||||
"message": "遊戲庫群組讓你可以將實例整理到遊戲庫中的不同分類。"
|
||||
},
|
||||
"instance.settings.tabs.general.library-groups.enter-name": {
|
||||
"message": "輸入群組名稱"
|
||||
@@ -447,10 +564,10 @@
|
||||
"message": "自訂啟動掛勾"
|
||||
},
|
||||
"instance.settings.tabs.hooks.description": {
|
||||
"message": "掛勾讓進階使用者能在遊戲啟動前和啟動後執行特定的系統命令。"
|
||||
"message": "掛勾讓進階使用者能在遊戲啟動前和啟動後執行特定的系統指令。"
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit": {
|
||||
"message": "結束後執行"
|
||||
"message": "結束後"
|
||||
},
|
||||
"instance.settings.tabs.hooks.post-exit.description": {
|
||||
"message": "遊戲關閉後執行。"
|
||||
@@ -575,6 +692,24 @@
|
||||
"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": "由該實例提供"
|
||||
},
|
||||
@@ -598,5 +733,26 @@
|
||||
},
|
||||
"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 等平臺分享模組包檔案來進行傳播。"
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.body": {
|
||||
"message": "我們在 Modrinth 上找不到這個檔案。強烈建議你僅安裝來自信任來源的檔案。"
|
||||
},
|
||||
"unknown-pack-warning-modal.warning.title": {
|
||||
"message": "未知檔案警告"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,46 +5,49 @@ import {
|
||||
ClipboardCopyIcon,
|
||||
ExternalIcon,
|
||||
GlobeIcon,
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
SpinnerIcon,
|
||||
StopCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { CardAction, ProjectType, Tags } from '@modrinth/ui'
|
||||
import type { BrowseInstallContentType, CardAction, ProjectType, Tags } from '@modrinth/ui'
|
||||
import {
|
||||
BrowsePageLayout,
|
||||
BrowseSidebar,
|
||||
commonMessages,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
getLatestMatchingInstallVersion,
|
||||
getSelectedInstallPreferences,
|
||||
getTargetInstallPreferences,
|
||||
injectNotificationManager,
|
||||
preferencesDiffer,
|
||||
provideBrowseManager,
|
||||
requestInstall,
|
||||
useBrowseSearch,
|
||||
useDebugLogger,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { get_project_v3, get_search_results_v3 } from '@/helpers/cache.js'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
|
||||
import {
|
||||
get_project,
|
||||
get_project_v3,
|
||||
get_search_results_v3,
|
||||
get_version_many,
|
||||
} from '@/helpers/cache.js'
|
||||
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import {
|
||||
get as getInstance,
|
||||
get_installed_project_ids as getInstalledProjectIds,
|
||||
kill,
|
||||
list as listInstances,
|
||||
} from '@/helpers/profile.js'
|
||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { add_server_to_profile, get_profile_worlds, getServerLatency } from '@/helpers/worlds'
|
||||
import { get_profile_worlds } from '@/helpers/worlds'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import {
|
||||
@@ -52,7 +55,6 @@ import {
|
||||
provideServerInstallContent,
|
||||
} from '@/providers/setup/server-install-content'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { getServerAddress } from '@/store/install.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -76,15 +78,27 @@ const {
|
||||
effectiveServerWorldId,
|
||||
serverContextServerData,
|
||||
serverContentProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
selectedServerInstallProjects,
|
||||
isInstallingQueuedServerInstalls,
|
||||
queuedInstallProgress,
|
||||
serverBackUrl,
|
||||
serverBackLabel,
|
||||
serverBrowseHeading,
|
||||
clearQueuedServerInstalls,
|
||||
removeQueuedServerInstall,
|
||||
flushQueuedServerInstalls,
|
||||
discardQueuedServerInstallsAndBack,
|
||||
installQueuedServerInstallsAndBack,
|
||||
initServerContext,
|
||||
watchServerContextChanges,
|
||||
searchServerModpacks,
|
||||
getServerProjectVersions,
|
||||
enforceSetupModpackRoute,
|
||||
installProjectToServer,
|
||||
getQueuedServerInstallPlans,
|
||||
setQueuedServerInstallPlans,
|
||||
openServerModpackInstallFlow,
|
||||
onServerFlowBack,
|
||||
handleServerModpackFlowCreate,
|
||||
markServerProjectInstalled,
|
||||
@@ -254,6 +268,7 @@ const instanceFilters = computed(() => {
|
||||
})
|
||||
|
||||
const serverHideInstalled = ref(false)
|
||||
const hideSelectedServerInstalls = ref(false)
|
||||
if (route.query.shi) {
|
||||
serverHideInstalled.value = route.query.shi === 'true'
|
||||
}
|
||||
@@ -291,6 +306,12 @@ const serverContextFilters = computed(() => {
|
||||
filters.push({ type: 'plugin_loader', option: platform })
|
||||
|
||||
if (pt === 'mod') filters.push({ type: 'environment', option: 'server' })
|
||||
|
||||
if (hideSelectedServerInstalls.value && queuedServerInstallProjectIds.value.size > 0) {
|
||||
for (const id of queuedServerInstallProjectIds.value) {
|
||||
filters.push({ type: 'project_id', option: `project_id:${id}`, negative: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pt === 'modpack') {
|
||||
@@ -313,134 +334,24 @@ const combinedProvidedFilters = computed(() =>
|
||||
isServerContext.value ? serverContextFilters.value : instanceFilters.value,
|
||||
)
|
||||
|
||||
const serverPings = shallowRef<Record<string, number | undefined>>({})
|
||||
const serverPingCache = new Map<string, number | undefined>()
|
||||
const pendingServerPings = new Map<string, Promise<number | undefined>>()
|
||||
let serverPingCacheActive = true
|
||||
const runningServerProjects = ref<Record<string, string>>({})
|
||||
|
||||
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('checkServerRunningStates', { hitCount: hits.length })
|
||||
const packs = await listInstances()
|
||||
const newRunning: Record<string, string> = {}
|
||||
for (const hit of hits) {
|
||||
const inst = packs.find((p: GameInstance) => p.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 playServerProject(projectId)
|
||||
checkServerRunningStates(lastServerHits.value)
|
||||
}
|
||||
|
||||
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
|
||||
|
||||
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 (instance.value) {
|
||||
try {
|
||||
await add_server_to_profile(
|
||||
instance.value.path,
|
||||
project.name,
|
||||
address,
|
||||
'prompt',
|
||||
project.project_id,
|
||||
project.minecraft_java_server?.content?.kind,
|
||||
)
|
||||
newlyInstalled.value.push(project.project_id)
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
} else {
|
||||
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((err) => {
|
||||
console.error(`Failed to ping server ${address}:`, err)
|
||||
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 }
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const unlistenProcesses = await process_listener(
|
||||
(e: { event: string; profile_path_id: string }) => {
|
||||
debugLog('process event', e)
|
||||
if (e.event === 'finished') {
|
||||
const projectId = Object.entries(runningServerProjects.value).find(
|
||||
([, path]) => path === e.profile_path_id,
|
||||
)?.[0]
|
||||
if (projectId) {
|
||||
const { [projectId]: _, ...rest } = runningServerProjects.value
|
||||
runningServerProjects.value = rest
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
serverPingCacheActive = false
|
||||
unlistenProcesses()
|
||||
serverPingCache.clear()
|
||||
pendingServerPings.clear()
|
||||
const {
|
||||
serverPings,
|
||||
contextMenuRef,
|
||||
updateServerHits,
|
||||
getServerModpackContent,
|
||||
getServerCardActions,
|
||||
handleRightClick,
|
||||
handleOptionsClick,
|
||||
} = useAppServerBrowse({
|
||||
instance,
|
||||
isFromWorlds,
|
||||
allInstalledIds,
|
||||
newlyInstalled,
|
||||
installingServerProjects,
|
||||
playServerProject,
|
||||
showAddServerToInstanceModal,
|
||||
handleError,
|
||||
router,
|
||||
})
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
@@ -454,29 +365,13 @@ window.addEventListener('online', () => {
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
addServerToInstance: {
|
||||
id: 'app.browse.add-server-to-instance',
|
||||
defaultMessage: 'Add server to instance',
|
||||
},
|
||||
addServersToInstance: {
|
||||
id: 'app.browse.add-servers-to-instance',
|
||||
defaultMessage: 'Add servers to your instance',
|
||||
defaultMessage: 'Adding server to instance',
|
||||
},
|
||||
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',
|
||||
addToAnInstance: {
|
||||
id: 'app.browse.add-to-an-instance',
|
||||
defaultMessage: 'Add to an instance',
|
||||
},
|
||||
discoverContent: {
|
||||
id: 'app.browse.discover-content',
|
||||
@@ -502,26 +397,19 @@ const messages = defineMessages({
|
||||
id: 'app.browse.hide-added-servers',
|
||||
defaultMessage: 'Hide already added servers',
|
||||
},
|
||||
hideInstalledContent: {
|
||||
id: 'app.browse.hide-installed-content',
|
||||
defaultMessage: 'Hide already installed content',
|
||||
},
|
||||
installContentToInstance: {
|
||||
id: 'app.browse.install-content-to-instance',
|
||||
defaultMessage: 'Install content to instance',
|
||||
},
|
||||
installToServer: {
|
||||
id: 'app.browse.server.install',
|
||||
defaultMessage: 'Install',
|
||||
},
|
||||
installedToServer: {
|
||||
id: 'app.browse.server.installed',
|
||||
defaultMessage: 'Installed',
|
||||
},
|
||||
installingToServer: {
|
||||
id: 'app.browse.server.installing',
|
||||
defaultMessage: 'Installing',
|
||||
},
|
||||
backToInstance: {
|
||||
id: 'app.browse.back-to-instance',
|
||||
defaultMessage: 'Back to instance',
|
||||
},
|
||||
serverInstanceContentWarning: {
|
||||
id: 'app.browse.server-instance-content-warning',
|
||||
defaultMessage:
|
||||
'Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content.',
|
||||
},
|
||||
modLoaderProvidedByInstance: {
|
||||
id: 'search.filter.locked.instance-loader.title',
|
||||
defaultMessage: 'Loader is provided by the instance',
|
||||
@@ -654,46 +542,6 @@ const selectableProjectTypes = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const 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
|
||||
? () => {
|
||||
router.push(`/project/${project_id}`)
|
||||
}
|
||||
: undefined,
|
||||
showCustomModpackTooltip: project_id === project.project_id,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const contextMenuRef = ref(null)
|
||||
// @ts-expect-error - no event types
|
||||
const handleRightClick = (event, result) => {
|
||||
// @ts-ignore
|
||||
contextMenuRef.value?.showMenu(event, result, [{ name: 'open_link' }, { name: 'copy_link' }])
|
||||
}
|
||||
// @ts-expect-error - no event types
|
||||
const handleOptionsClick = (args) => {
|
||||
switch (args.option) {
|
||||
case 'open_link':
|
||||
openUrl(`https://modrinth.com/${args.item.project_types?.[0] ?? 'project'}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.project_types?.[0] ?? 'project'}/${args.item.slug}`,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const installContext = computed(() => {
|
||||
if (isServerContext.value && serverContextServerData.value) {
|
||||
return {
|
||||
@@ -707,6 +555,15 @@ const installContext = computed(() => {
|
||||
backUrl: serverBackUrl.value,
|
||||
backLabel: serverBackLabel.value,
|
||||
heading: serverBrowseHeading.value,
|
||||
queuedCount: queuedServerInstallCount.value,
|
||||
selectedProjects: selectedServerInstallProjects.value,
|
||||
isInstallingSelected: isInstallingQueuedServerInstalls.value,
|
||||
installProgress: queuedInstallProgress.value,
|
||||
clearQueued: clearQueuedServerInstalls,
|
||||
clearSelected: clearQueuedServerInstalls,
|
||||
onBack: flushQueuedServerInstalls,
|
||||
discardSelectedAndBack: discardQueuedServerInstallsAndBack,
|
||||
installSelected: installQueuedServerInstallsAndBack,
|
||||
}
|
||||
}
|
||||
if (instance.value) {
|
||||
@@ -716,13 +573,13 @@ const installContext = computed(() => {
|
||||
gameVersion: instance.value.game_version,
|
||||
iconSrc: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||
backUrl: `/instance/${encodeURIComponent(instance.value.path)}${isFromWorlds.value ? '/worlds' : ''}`,
|
||||
backLabel: 'Back to instance',
|
||||
backLabel: formatMessage(messages.backToInstance),
|
||||
heading: formatMessage(
|
||||
isFromWorlds.value ? messages.addServersToInstance : messages.installContentToInstance,
|
||||
isFromWorlds.value ? messages.addServersToInstance : commonMessages.installingContentLabel,
|
||||
),
|
||||
warning:
|
||||
isServerInstance.value && !isFromWorlds.value
|
||||
? 'Adding content can break compatibility when joining the server. Any added content will also be lost when you update the server instance content.'
|
||||
? formatMessage(messages.serverInstanceContentWarning)
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
@@ -741,71 +598,82 @@ function setProjectInstalling(projectId: string, installing: boolean) {
|
||||
installingProjectIds.value = next
|
||||
}
|
||||
|
||||
const serverInstallQueue = {
|
||||
get: getQueuedServerInstallPlans,
|
||||
set: setQueuedServerInstallPlans,
|
||||
}
|
||||
|
||||
function getCurrentSelectedInstallPreferences(projectTypeValue: string) {
|
||||
return getSelectedInstallPreferences({
|
||||
contentType: projectTypeValue,
|
||||
selectedFilters: searchState.currentFilters.value,
|
||||
providedFilters: combinedProvidedFilters.value,
|
||||
overriddenProvidedFilterTypes: searchState.overriddenProvidedFilterTypes.value,
|
||||
})
|
||||
}
|
||||
|
||||
function getServerInstallTargetPreferences(contentType: BrowseInstallContentType) {
|
||||
return getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: serverContextServerData.value?.mc_version,
|
||||
loader: serverContextServerData.value?.loader,
|
||||
},
|
||||
contentType,
|
||||
)
|
||||
}
|
||||
|
||||
function getInstanceInstallTargetPreferences(projectTypeValue: string) {
|
||||
return getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: instance.value?.game_version,
|
||||
loader: instance.value?.loader,
|
||||
},
|
||||
projectTypeValue,
|
||||
)
|
||||
}
|
||||
|
||||
async function getInstallProjectVersions(projectId: string) {
|
||||
const project = await get_project(projectId, 'must_revalidate')
|
||||
return (await get_version_many(
|
||||
project.versions,
|
||||
'must_revalidate',
|
||||
)) as Labrinth.Versions.v2.Version[]
|
||||
}
|
||||
|
||||
async function chooseInstanceInstallVersion(
|
||||
project: Labrinth.Search.v2.ResultSearchProject & Labrinth.Search.v3.ResultSearchProject,
|
||||
projectTypeValue: string,
|
||||
) {
|
||||
const targetInstance = instance.value
|
||||
if (!targetInstance) {
|
||||
return { versionId: null as string | null }
|
||||
}
|
||||
|
||||
const selectedPreferences = getCurrentSelectedInstallPreferences(projectTypeValue)
|
||||
const targetPreferences = getInstanceInstallTargetPreferences(projectTypeValue)
|
||||
if (!preferencesDiffer(selectedPreferences, targetPreferences)) {
|
||||
return { versionId: null as string | null }
|
||||
}
|
||||
|
||||
const selectedVersion = getLatestMatchingInstallVersion(
|
||||
await getInstallProjectVersions(project.project_id),
|
||||
selectedPreferences,
|
||||
projectTypeValue,
|
||||
)
|
||||
|
||||
if (!selectedVersion) {
|
||||
return { versionId: null as string | null }
|
||||
}
|
||||
|
||||
return { versionId: selectedVersion.id }
|
||||
}
|
||||
|
||||
function getCardActions(
|
||||
result: Labrinth.Search.v2.ResultSearchProject | Labrinth.Search.v3.ResultSearchProject,
|
||||
currentProjectType: string,
|
||||
): CardAction[] {
|
||||
if (currentProjectType === 'server') {
|
||||
const serverResult = result as Labrinth.Search.v3.ResultSearchProject
|
||||
const isInstalled = allInstalledIds.value.has(serverResult.project_id)
|
||||
|
||||
if (isFromWorlds.value && 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)
|
||||
: instance.value
|
||||
? formatMessage(messages.addToInstanceName, { instanceName: instance.value.name })
|
||||
: formatMessage(messages.addServerToInstance),
|
||||
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 = (installingServerProjects.value as string[]).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
|
||||
return getServerCardActions(result as Labrinth.Search.v3.ResultSearchProject)
|
||||
}
|
||||
|
||||
// Non-server project actions
|
||||
@@ -817,39 +685,84 @@ function getCardActions(
|
||||
const isInstalled =
|
||||
projectResult.installed ||
|
||||
allInstalledIds.value.has(projectResult.project_id || '') ||
|
||||
serverContentProjectIds.value.has(projectResult.project_id || '')
|
||||
serverContentProjectIds.value.has(projectResult.project_id || '') ||
|
||||
serverContextServerData.value?.upstream?.project_id === projectResult.project_id
|
||||
const isInstalling = installingProjectIds.value.has(projectResult.project_id)
|
||||
|
||||
if (
|
||||
isServerContext.value &&
|
||||
['modpack', 'mod', 'plugin', 'datapack'].includes(currentProjectType)
|
||||
) {
|
||||
const isQueued = queuedServerInstallProjectIds.value.has(projectResult.project_id)
|
||||
const isInstallingSelection = isInstallingQueuedServerInstalls.value
|
||||
const validatingInstall =
|
||||
isInstalling && currentProjectType !== 'modpack' && !isInstallingSelection
|
||||
const installLabel = isInstalled
|
||||
? commonMessages.installedLabel
|
||||
: isQueued
|
||||
? isInstalling || isInstallingSelection
|
||||
? validatingInstall
|
||||
? commonMessages.validatingLabel
|
||||
: messages.installingToServer
|
||||
: commonMessages.selectedLabel
|
||||
: isInstalling || isInstallingSelection
|
||||
? validatingInstall
|
||||
? commonMessages.validatingLabel
|
||||
: messages.installingToServer
|
||||
: commonMessages.installButton
|
||||
return [
|
||||
{
|
||||
key: 'install',
|
||||
label: formatMessage(
|
||||
isInstalling
|
||||
? messages.installingToServer
|
||||
: isInstalled
|
||||
? messages.installedToServer
|
||||
: messages.installToServer,
|
||||
),
|
||||
icon: isInstalled ? CheckIcon : PlusIcon,
|
||||
iconClass: isInstalling ? 'animate-spin' : undefined,
|
||||
disabled: isInstalled || isInstalling,
|
||||
color: 'brand',
|
||||
label: formatMessage(installLabel),
|
||||
icon:
|
||||
isInstalling || isInstallingSelection
|
||||
? SpinnerIcon
|
||||
: isQueued || isInstalled
|
||||
? CheckIcon
|
||||
: PlusIcon,
|
||||
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
|
||||
disabled: isInstalled || isInstalling || isInstallingSelection,
|
||||
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
|
||||
type: 'outlined',
|
||||
onClick: async () => {
|
||||
setProjectInstalling(projectResult.project_id, true)
|
||||
if (isQueued) {
|
||||
removeQueuedServerInstall(projectResult.project_id)
|
||||
return
|
||||
}
|
||||
|
||||
const contentType = currentProjectType as BrowseInstallContentType
|
||||
const isModpack = contentType === 'modpack'
|
||||
const shouldShowInstalling = isModpack || !isQueued
|
||||
if (shouldShowInstalling) {
|
||||
setProjectInstalling(projectResult.project_id, true)
|
||||
}
|
||||
try {
|
||||
const didInstall = await installProjectToServer(projectResult)
|
||||
if (didInstall !== false) {
|
||||
onSearchResultInstalled(projectResult.project_id)
|
||||
}
|
||||
await requestInstall({
|
||||
project: projectResult,
|
||||
contentType,
|
||||
mode: isModpack ? 'immediate' : 'queue',
|
||||
selectedFilters: isModpack ? [] : searchState.currentFilters.value,
|
||||
providedFilters: isModpack ? [] : combinedProvidedFilters.value,
|
||||
overriddenProvidedFilterTypes: isModpack
|
||||
? []
|
||||
: searchState.overriddenProvidedFilterTypes.value,
|
||||
targetPreferences: getServerInstallTargetPreferences(contentType),
|
||||
getProjectVersions: getInstallProjectVersions,
|
||||
queue: serverInstallQueue,
|
||||
install: (plan) =>
|
||||
openServerModpackInstallFlow({
|
||||
projectId: plan.projectId,
|
||||
versionId: plan.versionId,
|
||||
name: plan.project.name,
|
||||
iconUrl: plan.project.icon_url ?? undefined,
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
} finally {
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
if (shouldShowInstalling) {
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -862,13 +775,15 @@ function getCardActions(
|
||||
return [
|
||||
{
|
||||
key: 'install',
|
||||
label: isInstalling
|
||||
? 'Installing'
|
||||
: isInstalled
|
||||
? 'Installed'
|
||||
: shouldUseInstallIcon
|
||||
? 'Install'
|
||||
: 'Add to an instance',
|
||||
label: formatMessage(
|
||||
isInstalling
|
||||
? messages.installingToServer
|
||||
: isInstalled
|
||||
? commonMessages.installedLabel
|
||||
: shouldUseInstallIcon
|
||||
? commonMessages.installButton
|
||||
: messages.addToAnInstance,
|
||||
),
|
||||
icon: isInstalling ? SpinnerIcon : isInstalled ? CheckIcon : PlusIcon,
|
||||
iconClass: isInstalling ? 'animate-spin' : undefined,
|
||||
disabled: isInstalled || isInstalling,
|
||||
@@ -876,28 +791,39 @@ function getCardActions(
|
||||
type: 'outlined',
|
||||
onClick: async () => {
|
||||
setProjectInstalling(projectResult.project_id, true)
|
||||
await installVersion(
|
||||
projectResult.project_id,
|
||||
null,
|
||||
instance.value ? instance.value.path : null,
|
||||
'SearchCard',
|
||||
(versionId) => {
|
||||
try {
|
||||
const selectedInstall = instance.value
|
||||
? await chooseInstanceInstallVersion(projectResult, currentProjectType)
|
||||
: { versionId: null as string | null }
|
||||
if (selectedInstall === null) {
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
if (versionId) {
|
||||
onSearchResultInstalled(projectResult.project_id)
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
{
|
||||
preferredLoader: instance.value?.loader ?? undefined,
|
||||
preferredGameVersion: instance.value?.game_version ?? undefined,
|
||||
},
|
||||
).catch((err) => {
|
||||
return
|
||||
}
|
||||
const selectedPreferences = getCurrentSelectedInstallPreferences(currentProjectType)
|
||||
await installVersion(
|
||||
projectResult.project_id,
|
||||
selectedInstall.versionId,
|
||||
instance.value ? instance.value.path : null,
|
||||
'SearchCard',
|
||||
(versionId) => {
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
if (versionId) {
|
||||
onSearchResultInstalled(projectResult.project_id)
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
{
|
||||
preferredLoader: instance.value?.loader ?? selectedPreferences.loaders?.[0],
|
||||
preferredGameVersion:
|
||||
instance.value?.game_version ?? selectedPreferences.gameVersions?.[0],
|
||||
},
|
||||
)
|
||||
} catch (err) {
|
||||
setProjectInstalling(projectResult.project_id, false)
|
||||
handleError(err)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -937,9 +863,7 @@ async function search(requestParams: string) {
|
||||
|
||||
if (isServer) {
|
||||
const hits = rawResults.result.hits ?? []
|
||||
lastServerHits.value = hits
|
||||
pingServerHits(hits)
|
||||
checkServerRunningStates(hits)
|
||||
updateServerHits(hits)
|
||||
return {
|
||||
projectHits: [],
|
||||
serverHits: hits,
|
||||
@@ -1024,6 +948,12 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(queuedServerInstallCount, (count) => {
|
||||
if (count === 0) {
|
||||
hideSelectedServerInstalls.value = false
|
||||
}
|
||||
})
|
||||
|
||||
if (instance.value?.game_version) {
|
||||
const gv = instance.value.game_version
|
||||
const alreadyHasGv = searchState.serverCurrentFilters.value.some(
|
||||
@@ -1036,16 +966,26 @@ if (instance.value?.game_version) {
|
||||
|
||||
await searchState.refreshSearch()
|
||||
|
||||
function getProjectBrowseQuery() {
|
||||
if (!installContext.value) return undefined
|
||||
return {
|
||||
...route.query,
|
||||
b: route.fullPath,
|
||||
}
|
||||
}
|
||||
|
||||
provideBrowseManager({
|
||||
tags,
|
||||
projectType,
|
||||
...searchState,
|
||||
getProjectLink: (result: Labrinth.Search.v2.ResultSearchProject) => ({
|
||||
path: `/project/${result.project_id ?? result.slug}`,
|
||||
query: instance.value ? { i: instance.value.path } : undefined,
|
||||
query: getProjectBrowseQuery(),
|
||||
}),
|
||||
getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) => ({
|
||||
path: `/project/${result.slug ?? result.project_id}`,
|
||||
query: getProjectBrowseQuery(),
|
||||
}),
|
||||
getServerProjectLink: (result: Labrinth.Search.v3.ResultSearchProject) =>
|
||||
`/project/${result.slug ?? result.project_id}`,
|
||||
selectableProjectTypes,
|
||||
showProjectTypeTabs: computed(() => !isServerContext.value),
|
||||
variant: 'app',
|
||||
@@ -1068,8 +1008,18 @@ provideBrowseManager({
|
||||
() => (isServerContext.value && projectType.value !== 'modpack') || !!instance.value,
|
||||
),
|
||||
hideInstalledLabel: computed(() =>
|
||||
formatMessage(isFromWorlds.value ? messages.hideAddedServers : messages.hideInstalledContent),
|
||||
formatMessage(
|
||||
isFromWorlds.value ? messages.hideAddedServers : commonMessages.hideInstalledContentLabel,
|
||||
),
|
||||
),
|
||||
hideSelected: hideSelectedServerInstalls,
|
||||
showHideSelected: computed(
|
||||
() =>
|
||||
isServerContext.value &&
|
||||
projectType.value !== 'modpack' &&
|
||||
queuedServerInstallCount.value > 0,
|
||||
),
|
||||
hideSelectedLabel: computed(() => formatMessage(commonMessages.hideSelectedContentLabel)),
|
||||
onInstalled: onSearchResultInstalled,
|
||||
serverPings,
|
||||
getServerModpackContent,
|
||||
@@ -1084,8 +1034,12 @@ provideBrowseManager({
|
||||
<BrowsePageLayout>
|
||||
<template #after>
|
||||
<ContextMenu ref="contextMenuRef" @option-clicked="handleOptionsClick">
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
<template #open_link>
|
||||
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
|
||||
</template>
|
||||
<template #copy_link>
|
||||
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
|
||||
</template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
</BrowsePageLayout>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
UpdatedIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
injectNotificationManager,
|
||||
@@ -383,25 +382,25 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
|
||||
@select="changeSkin(skin)"
|
||||
>
|
||||
<template #overlay-buttons>
|
||||
<Button
|
||||
color="green"
|
||||
aria-label="Edit skin"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(e: MouseEvent) => editSkinModal?.show(e, skin)"
|
||||
>
|
||||
<EditIcon /> Edit
|
||||
</Button>
|
||||
<Button
|
||||
v-show="!skin.is_equipped"
|
||||
v-tooltip="'Delete skin'"
|
||||
aria-label="Delete skin"
|
||||
color="red"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
icon-only
|
||||
@click.stop="() => confirmDeleteSkin(skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
aria-label="Edit skin"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(e: MouseEvent) => editSkinModal?.show(e, skin)"
|
||||
>
|
||||
<EditIcon /> Edit
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
|
||||
<button
|
||||
v-tooltip="'Delete skin'"
|
||||
aria-label="Delete skin"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="() => confirmDeleteSkin(skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
|
||||
@@ -311,7 +311,7 @@ async function updateProject(mod: ContentItem) {
|
||||
const profile = await get(props.instance.path).catch(handleError)
|
||||
|
||||
if (profile) {
|
||||
await installVersionDependencies(profile, versionData).catch(handleError)
|
||||
await installVersionDependencies(profile, versionData, 'update').catch(handleError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,7 +347,7 @@ async function switchProjectVersion(mod: ContentItem, version: Labrinth.Versions
|
||||
|
||||
const profile = await get(props.instance.path).catch(handleError)
|
||||
if (profile) {
|
||||
await installVersionDependencies(profile, version).catch(handleError)
|
||||
await installVersionDependencies(profile, version, 'update').catch(handleError)
|
||||
}
|
||||
|
||||
mod.file_path = newPath
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<button class="!h-10" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
@@ -141,7 +141,7 @@
|
||||
>
|
||||
<template #actions>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!h-10 !border-button-bg !border-[1px]" @click="addServerModal?.show()">
|
||||
<button class="!h-10" @click="addServerModal?.show()">
|
||||
<PlusIcon class="size-5" />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager, NavTabs } from '@modrinth/ui'
|
||||
import { ButtonStyled, injectNotificationManager, NavTabs } from '@modrinth/ui'
|
||||
import { inject, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
@@ -55,10 +55,12 @@ onUnmounted(() => {
|
||||
<NewInstanceImage />
|
||||
</div>
|
||||
<h3>No instances found</h3>
|
||||
<Button color="primary" :disabled="offline" @click="showCreationModal?.()">
|
||||
<PlusIcon />
|
||||
Create new instance
|
||||
</Button>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="offline" @click="showCreationModal?.()">
|
||||
<PlusIcon />
|
||||
Create new instance
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -39,35 +39,40 @@
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<Button class="close" icon-only @click="hideImage">
|
||||
<XIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<a
|
||||
class="open btn icon-only"
|
||||
target="_blank"
|
||||
:href="
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
</a>
|
||||
<Button icon-only @click="zoomedIn = !zoomedIn">
|
||||
<ExpandIcon v-if="!zoomedIn" aria-hidden="true" />
|
||||
<ContractIcon v-else aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="filteredGallery.length > 1"
|
||||
class="previous"
|
||||
icon-only
|
||||
@click="previousImage()"
|
||||
>
|
||||
<LeftArrowIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<Button v-if="filteredGallery.length > 1" class="next" icon-only @click="nextImage()">
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<ButtonStyled circular>
|
||||
<button class="close" @click="hideImage">
|
||||
<XIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<a
|
||||
class="open btn icon-only"
|
||||
target="_blank"
|
||||
:href="
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<button @click="zoomedIn = !zoomedIn">
|
||||
<ExpandIcon v-if="!zoomedIn" aria-hidden="true" />
|
||||
<ContractIcon v-else aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="filteredGallery.length > 1" circular>
|
||||
<button class="previous" @click="previousImage()">
|
||||
<LeftArrowIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="filteredGallery.length > 1" circular>
|
||||
<button class="next" @click="nextImage()">
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,7 +90,7 @@ import {
|
||||
RightArrowIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, Card, useFormatDateTime } from '@modrinth/ui'
|
||||
import { ButtonStyled, Card, useFormatDateTime } from '@modrinth/ui'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
|
||||
@@ -45,7 +45,13 @@
|
||||
/>
|
||||
</Teleport>
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||
<div
|
||||
v-if="projectInstallContext"
|
||||
class="sticky top-0 z-20 -mx-6 -mt-6 rounded-tl-[--radius-xl] border-0 border-b border-solid bg-surface-1 p-3 border-surface-5"
|
||||
>
|
||||
<BrowseInstallHeader :install-context="projectInstallContext" />
|
||||
</div>
|
||||
<InstanceIndicator v-if="instance && !projectInstallContext" :instance="instance" />
|
||||
<template v-if="data">
|
||||
<Teleport
|
||||
v-if="themeStore.featureFlags.project_background"
|
||||
@@ -64,7 +70,7 @@
|
||||
<ButtonStyled v-if="serverPlaying" size="large" color="red">
|
||||
<button @click="handleStopServer">
|
||||
<StopCircleIcon />
|
||||
Stop
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else size="large" color="brand">
|
||||
@@ -73,11 +79,18 @@
|
||||
@click="handleClickPlay"
|
||||
>
|
||||
<PlayIcon />
|
||||
{{ data && installingServerProjects.includes(data.id) ? 'Installing...' : 'Play' }}
|
||||
{{
|
||||
data && installingServerProjects.includes(data.id)
|
||||
? formatMessage(commonMessages.installingLabel)
|
||||
: formatMessage(commonMessages.playButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular>
|
||||
<button v-tooltip="'Add server to instance'" @click="handleAddServerToInstance">
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.addServerToInstanceButton)"
|
||||
@click="handleAddServerToInstance"
|
||||
>
|
||||
<PlusIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -111,13 +124,17 @@
|
||||
<template v-else #actions>
|
||||
<ButtonStyled size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installed ? `This project is already installed` : null"
|
||||
:disabled="installed || installing"
|
||||
v-tooltip="installButtonTooltip"
|
||||
:disabled="installButtonDisabled"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
<SpinnerIcon
|
||||
v-if="installButtonLoading && !installButtonInstalled"
|
||||
class="animate-spin"
|
||||
/>
|
||||
<DownloadIcon v-else-if="!installButtonInstalled && !serverProjectSelected" />
|
||||
<CheckIcon v-else />
|
||||
{{ installButtonLabel }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
@@ -166,7 +183,7 @@
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}`,
|
||||
href: projectDescriptionHref,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
@@ -176,7 +193,7 @@
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/project/${$route.params.id}/gallery`,
|
||||
href: projectGalleryHref,
|
||||
shown: data.gallery.length > 0,
|
||||
},
|
||||
]"
|
||||
@@ -195,11 +212,39 @@
|
||||
</template>
|
||||
<template v-else> Project data couldn't not be loaded. </template>
|
||||
</div>
|
||||
<SelectedProjectsFloatingBar
|
||||
v-if="projectInstallContext"
|
||||
:install-context="projectInstallContext"
|
||||
/>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
<template #install>
|
||||
<DownloadIcon /> {{ formatMessage(commonMessages.installButton) }}
|
||||
</template>
|
||||
<template #open_link>
|
||||
<GlobeIcon /> {{ formatMessage(commonMessages.openInModrinthButton) }} <ExternalIcon />
|
||||
</template>
|
||||
<template #copy_link>
|
||||
<ClipboardCopyIcon /> {{ formatMessage(commonMessages.copyLinkButton) }}
|
||||
</template>
|
||||
</ContextMenu>
|
||||
<CreationFlowModal
|
||||
v-if="serverInstallContent.isServerContext.value && data?.project_type === 'modpack'"
|
||||
ref="serverSetupModalRef"
|
||||
:type="
|
||||
serverInstallContent.serverFlowFrom.value === 'reset-server'
|
||||
? 'reset-server'
|
||||
: 'server-onboarding'
|
||||
"
|
||||
:available-loaders="['vanilla', 'fabric', 'neoforge', 'forge', 'quilt', 'paper', 'purpur']"
|
||||
:show-snapshot-toggle="true"
|
||||
:on-back="serverInstallContent.onServerFlowBack"
|
||||
:search-modpacks="serverInstallContent.searchServerModpacks"
|
||||
:get-project-versions="serverInstallContent.getServerProjectVersions"
|
||||
:get-loader-manifest="getLoaderManifest"
|
||||
@hide="() => {}"
|
||||
@browse-modpacks="() => {}"
|
||||
@create="serverInstallContent.handleServerModpackFlowCreate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -216,10 +261,16 @@ import {
|
||||
PlayIcon,
|
||||
PlusIcon,
|
||||
ReportIcon,
|
||||
SpinnerIcon,
|
||||
StopCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
BrowseInstallHeader,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
getTargetInstallPreferences,
|
||||
injectNotificationManager,
|
||||
NavTabs,
|
||||
OverflowMenu,
|
||||
@@ -231,7 +282,11 @@ import {
|
||||
ProjectSidebarLinks,
|
||||
ProjectSidebarServerInfo,
|
||||
ProjectSidebarTags,
|
||||
requestInstall,
|
||||
SelectedProjectsFloatingBar,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
@@ -249,6 +304,7 @@ import {
|
||||
get_version_many,
|
||||
} from '@/helpers/cache.js'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { get_loader_versions as getLoaderManifest } from '@/helpers/metadata'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
import {
|
||||
get as getInstance,
|
||||
@@ -260,6 +316,7 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { getServerLatency } from '@/helpers/worlds'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
import { createServerInstallContent } from '@/providers/setup/server-install-content'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { getServerAddress } from '@/store/install.js'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
@@ -272,6 +329,22 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
backToBrowse: {
|
||||
id: 'app.project.install-context.back-to-browse',
|
||||
defaultMessage: 'Back to browse',
|
||||
},
|
||||
installContentToInstance: {
|
||||
id: 'app.project.install-context.install-content-to-instance',
|
||||
defaultMessage: 'Install content to instance',
|
||||
},
|
||||
alreadyInstalled: {
|
||||
id: 'app.project.install-button.already-installed',
|
||||
defaultMessage: 'This project is already installed',
|
||||
},
|
||||
})
|
||||
|
||||
const { installingServerProjects, playServerProject, showAddServerToInstanceModal } =
|
||||
injectServerInstall()
|
||||
@@ -296,6 +369,11 @@ const serverPing = ref(undefined)
|
||||
const serverStatusOnline = ref(false)
|
||||
const serverInstancePath = ref(null)
|
||||
const serverPlaying = ref(false)
|
||||
const serverSetupModalRef = ref(null)
|
||||
const serverInstallContent = createServerInstallContent({ serverSetupModalRef })
|
||||
|
||||
serverInstallContent.watchServerContextChanges()
|
||||
await serverInstallContent.initServerContext()
|
||||
|
||||
const instanceFilters = computed(() => {
|
||||
if (!instance.value) {
|
||||
@@ -315,11 +393,9 @@ const instanceFilters = computed(() => {
|
||||
return { l: loaders, g: instance.value.game_version }
|
||||
})
|
||||
|
||||
const versionsHref = computed(() => {
|
||||
const base = `/project/${route.params.id}/versions`
|
||||
const filters = instanceFilters.value
|
||||
function buildProjectHref(path, extraQuery = {}) {
|
||||
const params = new URLSearchParams()
|
||||
for (const [key, val] of Object.entries(filters)) {
|
||||
for (const [key, val] of Object.entries({ ...route.query, ...extraQuery })) {
|
||||
if (Array.isArray(val)) {
|
||||
for (const v of val) params.append(key, v)
|
||||
} else if (val) {
|
||||
@@ -327,7 +403,102 @@ const versionsHref = computed(() => {
|
||||
}
|
||||
}
|
||||
const qs = params.toString()
|
||||
return qs ? `${base}?${qs}` : base
|
||||
return qs ? `${path}?${qs}` : path
|
||||
}
|
||||
|
||||
const projectDescriptionHref = computed(() => buildProjectHref(`/project/${route.params.id}`))
|
||||
const versionsHref = computed(() =>
|
||||
buildProjectHref(`/project/${route.params.id}/versions`, instanceFilters.value),
|
||||
)
|
||||
const projectGalleryHref = computed(() => buildProjectHref(`/project/${route.params.id}/gallery`))
|
||||
|
||||
const projectBrowseBackUrl = computed(() => {
|
||||
const browsePath = route.query.b
|
||||
if (typeof browsePath === 'string' && browsePath.startsWith('/browse/')) return browsePath
|
||||
const type = data.value?.project_type ? `${data.value.project_type}s` : 'mods'
|
||||
return `/browse/${type}`
|
||||
})
|
||||
|
||||
const projectInstallContext = computed(() => {
|
||||
const serverData = serverInstallContent.serverContextServerData.value
|
||||
if (serverData) {
|
||||
return {
|
||||
name: serverData.name,
|
||||
loader: serverData.loader ?? '',
|
||||
gameVersion: serverData.mc_version ?? '',
|
||||
serverId: serverInstallContent.serverIdQuery.value,
|
||||
upstream: serverData.upstream,
|
||||
iconSrc: null,
|
||||
isMedal: serverData.is_medal,
|
||||
backUrl: projectBrowseBackUrl.value,
|
||||
backLabel: formatMessage(messages.backToBrowse),
|
||||
heading: serverInstallContent.serverBrowseHeading.value,
|
||||
queuedCount: serverInstallContent.queuedServerInstallCount.value,
|
||||
selectedProjects: serverInstallContent.selectedServerInstallProjects.value,
|
||||
isInstallingSelected: serverInstallContent.isInstallingQueuedServerInstalls.value,
|
||||
installProgress: serverInstallContent.queuedInstallProgress.value,
|
||||
clearQueued: serverInstallContent.clearQueuedServerInstalls,
|
||||
clearSelected: serverInstallContent.clearQueuedServerInstalls,
|
||||
discardSelectedAndBack: serverInstallContent.discardQueuedServerInstallsAndBack,
|
||||
installSelected: serverInstallContent.installQueuedServerInstallsAndBack,
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.value) {
|
||||
return {
|
||||
name: instance.value.name,
|
||||
loader: instance.value.loader,
|
||||
gameVersion: instance.value.game_version,
|
||||
iconSrc: instance.value.icon_path ? convertFileSrc(instance.value.icon_path) : null,
|
||||
backUrl: projectBrowseBackUrl.value,
|
||||
backLabel: formatMessage(messages.backToBrowse),
|
||||
heading: formatMessage(messages.installContentToInstance),
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const serverProjectInstallContext = computed(
|
||||
() =>
|
||||
!!serverInstallContent.serverContextServerData.value &&
|
||||
['modpack', 'mod', 'plugin', 'datapack'].includes(data.value?.project_type),
|
||||
)
|
||||
const serverProjectSelected = computed(
|
||||
() => !!data.value && serverInstallContent.queuedServerInstallProjectIds.value.has(data.value.id),
|
||||
)
|
||||
const serverProjectInstalled = computed(
|
||||
() =>
|
||||
!!data.value &&
|
||||
(serverInstallContent.serverContentProjectIds.value.has(data.value.id) ||
|
||||
serverInstallContent.serverContextServerData.value?.upstream?.project_id === data.value.id),
|
||||
)
|
||||
const installButtonLoading = computed(
|
||||
() => installing.value || serverInstallContent.isInstallingQueuedServerInstalls.value,
|
||||
)
|
||||
const installButtonValidating = computed(
|
||||
() =>
|
||||
serverProjectInstallContext.value &&
|
||||
installing.value &&
|
||||
data.value?.project_type !== 'modpack' &&
|
||||
!serverInstallContent.isInstallingQueuedServerInstalls.value,
|
||||
)
|
||||
const installButtonInstalled = computed(() =>
|
||||
serverProjectInstallContext.value ? serverProjectInstalled.value : installed.value,
|
||||
)
|
||||
const installButtonDisabled = computed(
|
||||
() => installButtonInstalled.value || installButtonLoading.value,
|
||||
)
|
||||
const installButtonLabel = computed(() => {
|
||||
if (installButtonInstalled.value) return formatMessage(commonMessages.installedLabel)
|
||||
if (installButtonValidating.value) return formatMessage(commonMessages.validatingLabel)
|
||||
if (installButtonLoading.value) return formatMessage(commonMessages.installingLabel)
|
||||
if (serverProjectSelected.value) return formatMessage(commonMessages.selectedLabel)
|
||||
return formatMessage(commonMessages.installButton)
|
||||
})
|
||||
const installButtonTooltip = computed(() => {
|
||||
if (installButtonInstalled.value) return formatMessage(messages.alreadyInstalled)
|
||||
return null
|
||||
})
|
||||
|
||||
const [allLoaders, allGameVersions] = await Promise.all([
|
||||
@@ -499,6 +670,55 @@ watch(
|
||||
)
|
||||
|
||||
async function install(version) {
|
||||
if (serverProjectInstallContext.value && data.value) {
|
||||
if (serverProjectSelected.value) {
|
||||
serverInstallContent.removeQueuedServerInstall(data.value.id)
|
||||
return
|
||||
}
|
||||
if (installButtonDisabled.value) return
|
||||
|
||||
installing.value = true
|
||||
try {
|
||||
const contentType = data.value.project_type
|
||||
await requestInstall({
|
||||
project: {
|
||||
...data.value,
|
||||
project_id: data.value.id,
|
||||
icon_url: data.value.icon_url,
|
||||
},
|
||||
contentType,
|
||||
mode: contentType === 'modpack' ? 'immediate' : 'queue',
|
||||
selectedFilters: [],
|
||||
providedFilters: [],
|
||||
overriddenProvidedFilterTypes: [],
|
||||
targetPreferences: getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: serverInstallContent.serverContextServerData.value?.mc_version,
|
||||
loader: serverInstallContent.serverContextServerData.value?.loader,
|
||||
},
|
||||
contentType,
|
||||
),
|
||||
getProjectVersions: async () => versions.value,
|
||||
queue: {
|
||||
get: serverInstallContent.getQueuedServerInstallPlans,
|
||||
set: serverInstallContent.setQueuedServerInstallPlans,
|
||||
},
|
||||
install: (plan) =>
|
||||
serverInstallContent.openServerModpackInstallFlow({
|
||||
projectId: plan.projectId,
|
||||
versionId: plan.versionId,
|
||||
name: plan.project.title ?? plan.project.name ?? data.value.title,
|
||||
iconUrl: plan.project.icon_url ?? undefined,
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
} finally {
|
||||
installing.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
data.value.id,
|
||||
|
||||
@@ -14,34 +14,38 @@
|
||||
<h2>{{ version.name }}</h2>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button
|
||||
color="primary"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installing || (installed && installedVersion === version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installedVersion !== version.id" />
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing...'
|
||||
: installed && installedVersion === version.id
|
||||
? 'Installed'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
<Button>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</Button>
|
||||
<a
|
||||
:href="`https://modrinth.com/mod/${route.params.id}/version/${route.params.version}`"
|
||||
rel="external"
|
||||
class="btn"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Modrinth website
|
||||
</a>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:disabled="installing || (installed && installedVersion === version.id)"
|
||||
@click="() => install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installedVersion !== version.id" />
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing...'
|
||||
: installed && installedVersion === version.id
|
||||
? 'Installed'
|
||||
: 'Install'
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
:href="`https://modrinth.com/mod/${route.params.id}/version/${route.params.version}`"
|
||||
rel="external"
|
||||
>
|
||||
Modrinth website
|
||||
<ExternalIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="version-container">
|
||||
@@ -68,16 +72,13 @@
|
||||
<span v-if="file.primary" class="primary-label"> Primary </span>
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
v-if="project.project_type !== 'modpack' || file.primary"
|
||||
class="download"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installed"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
<ButtonStyled v-if="project.project_type !== 'modpack' || file.primary" color="brand">
|
||||
<button class="download" :disabled="installed" @click="() => install(version.id)">
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</Card>
|
||||
</Card>
|
||||
<Card v-if="displayDependencies.length > 0">
|
||||
@@ -168,8 +169,17 @@
|
||||
|
||||
<script setup>
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, FileIcon, ReportIcon } from '@modrinth/assets'
|
||||
import { Avatar, Badge, Breadcrumbs, Button, Card, CopyCode, useFormatDateTime } from '@modrinth/ui'
|
||||
import { formatBytes, renderString } from '@modrinth/utils'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Breadcrumbs,
|
||||
ButtonStyled,
|
||||
Card,
|
||||
CopyCode,
|
||||
useFormatBytes,
|
||||
useFormatDateTime,
|
||||
} from '@modrinth/ui'
|
||||
import { renderString } from '@modrinth/utils'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
@@ -182,6 +192,7 @@ const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
|
||||
@@ -134,10 +134,6 @@ const [loaders, gameVersions] = await Promise.all([
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-grow: 1;
|
||||
|
||||
.multiselect {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-row {
|
||||
|
||||
@@ -430,6 +430,7 @@ export function createContentInstall(opts: {
|
||||
await installVersionDependencies(
|
||||
profile,
|
||||
version,
|
||||
'dependency',
|
||||
(depProject: Labrinth.Projects.v2.Project, depVersion?: Labrinth.Versions.v2.Version) => {
|
||||
addInstallingItem(instance.id, depProject, depVersion)
|
||||
installedProjectIds.push(depProject.id)
|
||||
@@ -485,10 +486,10 @@ export function createContentInstall(opts: {
|
||||
if (!id) return
|
||||
|
||||
await add_project_from_version(id, version.id, 'standalone')
|
||||
await opts.router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||
await opts.router.push(`/instance/${encodeURIComponent(id)}`)
|
||||
|
||||
const instance = await get(id)
|
||||
await installVersionDependencies(instance, version)
|
||||
await installVersionDependencies(instance, version, 'dependency')
|
||||
|
||||
trackEvent('InstanceCreate', {
|
||||
source: 'ProjectInstallModal',
|
||||
@@ -512,7 +513,7 @@ export function createContentInstall(opts: {
|
||||
|
||||
function handleNavigate(instance: ContentInstallInstance) {
|
||||
modalRef?.hide()
|
||||
opts.router.push(`/instance/${encodeURIComponent(instance.id)}/`)
|
||||
opts.router.push(`/instance/${encodeURIComponent(instance.id)}`)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
@@ -589,6 +590,7 @@ export function createContentInstall(opts: {
|
||||
await installVersionDependencies(
|
||||
instance,
|
||||
version,
|
||||
'dependency',
|
||||
(
|
||||
depProject: Labrinth.Projects.v2.Project,
|
||||
depVersion?: Labrinth.Versions.v2.Version,
|
||||
@@ -664,7 +666,7 @@ export function createContentInstall(opts: {
|
||||
},
|
||||
handleModpackDuplicateGoToInstance(instancePath: string) {
|
||||
pendingModpackInstall = null
|
||||
opts.router.push(`/instance/${encodeURIComponent(instancePath)}/`)
|
||||
opts.router.push(`/instance/${encodeURIComponent(instancePath)}`)
|
||||
},
|
||||
setIncompatibilityWarningModal(ref: IncompatibilityWarningModalRef) {
|
||||
incompatibilityWarningModalRef = ref
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { provideFilePicker } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { readFile } from '@tauri-apps/plugin-fs'
|
||||
|
||||
function getFileName(path: string, fallback: string) {
|
||||
return path.split(/[\\/]/).pop() || fallback
|
||||
}
|
||||
|
||||
async function createFileFromPath(path: string, fallbackName: string, type?: string) {
|
||||
const bytes = await readFile(path)
|
||||
const name = getFileName(path, fallbackName)
|
||||
return new File([bytes], name, type ? { type } : undefined)
|
||||
}
|
||||
|
||||
export function setupFilePickerProvider() {
|
||||
provideFilePicker({
|
||||
@@ -12,8 +23,7 @@ export function setupFilePickerProvider() {
|
||||
if (!result) return null
|
||||
const path = result.path ?? result
|
||||
if (!path) return null
|
||||
const name = path.split(/[\\/]/).pop() || 'icon'
|
||||
const file = new File([], name)
|
||||
const file = await createFileFromPath(path, 'icon')
|
||||
return { file, path, previewUrl: convertFileSrc(path) }
|
||||
},
|
||||
async pickModpackFile() {
|
||||
@@ -24,8 +34,11 @@ export function setupFilePickerProvider() {
|
||||
if (!result) return null
|
||||
const path = result.path ?? result
|
||||
if (!path) return null
|
||||
const name = path.split(/[\\/]/).pop() || 'modpack.mrpack'
|
||||
const file = new File([], name)
|
||||
const file = await createFileFromPath(
|
||||
path,
|
||||
'modpack.mrpack',
|
||||
'application/x-modrinth-modpack+zip',
|
||||
)
|
||||
return { file, path, previewUrl: '' }
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import type { AbstractModrinthClient, Archon, Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
addPendingServerContentInstalls,
|
||||
type BrowseInstallPlan,
|
||||
type BrowseSelectedProject,
|
||||
createContext,
|
||||
type CreationFlowContextValue,
|
||||
flushInstallQueue,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
type PendingServerContentInstall,
|
||||
type PendingServerContentInstallType,
|
||||
readPendingServerContentInstalls,
|
||||
removePendingServerContentInstall,
|
||||
writePendingServerContentInstallBaseline,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, type ComputedRef, nextTick, type Ref, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
type ServerFlowFrom = 'onboarding' | 'reset-server'
|
||||
type ServerInstallableType = 'modpack' | 'mod' | 'plugin' | 'datapack'
|
||||
|
||||
type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & {
|
||||
title?: string
|
||||
installing?: boolean
|
||||
installed?: boolean
|
||||
}
|
||||
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
|
||||
|
||||
interface ServerModpackSelectionRequest {
|
||||
export interface ServerModpackSelectionRequest {
|
||||
projectId: string
|
||||
versionId: string
|
||||
name: string
|
||||
@@ -40,9 +50,19 @@ export interface ServerInstallContentContext {
|
||||
effectiveServerWorldId: ComputedRef<string | null>
|
||||
serverContextServerData: Ref<Archon.Servers.v0.Server | null>
|
||||
serverContentProjectIds: Ref<Set<string>>
|
||||
queuedServerInstallProjectIds: ComputedRef<Set<string>>
|
||||
queuedServerInstallCount: ComputedRef<number>
|
||||
selectedServerInstallProjects: ComputedRef<BrowseSelectedProject[]>
|
||||
isInstallingQueuedServerInstalls: Ref<boolean>
|
||||
queuedInstallProgress: Ref<{ completed: number; total: number }>
|
||||
serverBackUrl: ComputedRef<string>
|
||||
serverBackLabel: ComputedRef<string>
|
||||
serverBrowseHeading: ComputedRef<string>
|
||||
clearQueuedServerInstalls: () => void
|
||||
removeQueuedServerInstall: (projectId: string) => void
|
||||
flushQueuedServerInstalls: () => Promise<boolean>
|
||||
discardQueuedServerInstallsAndBack: () => Promise<void>
|
||||
installQueuedServerInstallsAndBack: () => Promise<boolean>
|
||||
initServerContext: () => Promise<void>
|
||||
watchServerContextChanges: () => void
|
||||
searchServerModpacks: (
|
||||
@@ -51,7 +71,11 @@ export interface ServerInstallContentContext {
|
||||
) => Promise<Labrinth.Projects.v2.SearchResult>
|
||||
getServerProjectVersions: (projectId: string) => Promise<{ id: string }[]>
|
||||
enforceSetupModpackRoute: (currentProjectType: string | undefined) => void
|
||||
installProjectToServer: (project: InstallableSearchResult) => Promise<boolean>
|
||||
getQueuedServerInstallPlans: () => Map<string, BrowseInstallPlan<InstallableSearchResult>>
|
||||
setQueuedServerInstallPlans: (
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) => void
|
||||
openServerModpackInstallFlow: (request: ServerModpackSelectionRequest) => Promise<void>
|
||||
onServerFlowBack: () => void
|
||||
handleServerModpackFlowCreate: (config: CreationFlowContextValue) => Promise<void>
|
||||
markServerProjectInstalled: (id: string) => void
|
||||
@@ -65,6 +89,145 @@ function readQueryString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.length > 0 ? value : null
|
||||
}
|
||||
|
||||
function getQueueStorageKey(serverId: string | null, worldId: string | null) {
|
||||
if (!serverId || !worldId) return null
|
||||
return `server-install-queue:${serverId}:${worldId}`
|
||||
}
|
||||
|
||||
function readStoredQueue(serverId: string | null, worldId: string | null) {
|
||||
const key = getQueueStorageKey(serverId, worldId)
|
||||
if (!key) return new Map<string, BrowseInstallPlan<InstallableSearchResult>>()
|
||||
try {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (!raw) return new Map<string, BrowseInstallPlan<InstallableSearchResult>>()
|
||||
return new Map<string, BrowseInstallPlan<InstallableSearchResult>>(JSON.parse(raw))
|
||||
} catch {
|
||||
return new Map<string, BrowseInstallPlan<InstallableSearchResult>>()
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredQueue(
|
||||
serverId: string | null,
|
||||
worldId: string | null,
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
const key = getQueueStorageKey(serverId, worldId)
|
||||
if (!key) return
|
||||
if (plans.size === 0) {
|
||||
localStorage.removeItem(key)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(Array.from(plans.entries())))
|
||||
}
|
||||
|
||||
function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
|
||||
if (project.organization) {
|
||||
const ownerId = project.organization_id ?? project.organization
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.organization,
|
||||
type: 'organization' as const,
|
||||
link: `https://modrinth.com/organization/${ownerId}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.author) return null
|
||||
|
||||
const ownerId = project.author_id ?? project.author
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.author,
|
||||
type: 'user' as const,
|
||||
link: `https://modrinth.com/user/${ownerId}`,
|
||||
}
|
||||
}
|
||||
|
||||
async function getQueuedInstallOwner(
|
||||
client: AbstractModrinthClient,
|
||||
project: InstallableSearchResult,
|
||||
) {
|
||||
const fallback = getQueuedInstallOwnerFallback(project)
|
||||
|
||||
try {
|
||||
if (project.organization) {
|
||||
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
|
||||
if (organization) {
|
||||
return {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
type: 'organization' as const,
|
||||
avatar_url: organization.icon_url ?? undefined,
|
||||
link: `https://modrinth.com/organization/${organization.slug}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
|
||||
const owner =
|
||||
members.find((member) => member.user.id === project.author_id)?.user ??
|
||||
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
|
||||
members[0]?.user
|
||||
|
||||
if (owner) {
|
||||
return {
|
||||
id: owner.id,
|
||||
name: owner.username,
|
||||
type: 'user' as const,
|
||||
avatar_url: owner.avatar_url,
|
||||
link: `https://modrinth.com/user/${owner.username}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getQueuedAddonInstallPlans(
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholder(
|
||||
plan: BrowseInstallPlan<InstallableSearchResult>,
|
||||
owner: PendingServerContentInstallInput['owner'],
|
||||
): PendingServerContentInstallInput {
|
||||
const project = plan.project as InstallableSearchResult & { slug?: string | null }
|
||||
return {
|
||||
projectId: plan.projectId,
|
||||
versionId: plan.versionId,
|
||||
contentType: plan.contentType as PendingServerContentInstallType,
|
||||
title: project.title ?? project.name ?? 'Project',
|
||||
versionName: plan.versionName ?? null,
|
||||
versionNumber: plan.versionNumber ?? null,
|
||||
fileName: plan.fileName ?? null,
|
||||
owner,
|
||||
slug: project.slug ?? plan.projectId,
|
||||
iconUrl: project.icon_url ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholderFallbacks(
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
return getQueuedAddonInstallPlans(plans).map((plan) =>
|
||||
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
|
||||
)
|
||||
}
|
||||
|
||||
async function getQueuedInstallPlaceholders(
|
||||
client: AbstractModrinthClient,
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
return Promise.all(
|
||||
getQueuedAddonInstallPlans(plans).map(async (plan) =>
|
||||
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(client, plan.project)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function createServerInstallContent(opts: {
|
||||
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
|
||||
}) {
|
||||
@@ -72,7 +235,7 @@ export function createServerInstallContent(opts: {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const client = injectModrinthClient()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { addNotification, handleError } = injectNotificationManager()
|
||||
|
||||
const serverIdQuery = computed(() => readQueryString(route.query.sid))
|
||||
const worldIdQuery = computed(() => readQueryString(route.query.wid))
|
||||
@@ -90,8 +253,22 @@ export function createServerInstallContent(opts: {
|
||||
const serverContextWorldId = ref<string | null>(worldIdQuery.value)
|
||||
const serverContextServerData = ref<Archon.Servers.v0.Server | null>(null)
|
||||
const serverContentProjectIds = ref<Set<string>>(new Set())
|
||||
const serverContentInstallKeys = ref<Set<string>>(new Set())
|
||||
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
|
||||
new Map(),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
||||
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
||||
const selectedServerInstallProjects = computed<BrowseSelectedProject[]>(() =>
|
||||
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
|
||||
id: plan.projectId,
|
||||
name: plan.project.title ?? plan.project.name ?? 'Project',
|
||||
iconUrl: plan.project.icon_url ?? null,
|
||||
})),
|
||||
)
|
||||
const isInstallingQueuedServerInstalls = ref(false)
|
||||
const queuedInstallProgress = ref({ completed: 0, total: 0 })
|
||||
const effectiveServerWorldId = computed(() => worldIdQuery.value ?? serverContextWorldId.value)
|
||||
|
||||
const serverBackUrl = computed(() => {
|
||||
const sid = serverIdQuery.value
|
||||
if (!sid) return '/hosting/manage'
|
||||
@@ -110,9 +287,9 @@ export function createServerInstallContent(opts: {
|
||||
})
|
||||
const serverBrowseHeading = computed(() => {
|
||||
if (serverFlowFrom.value === 'reset-server') {
|
||||
return 'Select modpack to install after reset'
|
||||
return 'Selecting modpack to install after reset'
|
||||
}
|
||||
return 'Install content to server'
|
||||
return 'Installing content'
|
||||
})
|
||||
|
||||
async function resolveServerContextWorldId(serverId: string) {
|
||||
@@ -134,7 +311,11 @@ export function createServerInstallContent(opts: {
|
||||
.map((addon) => addon.project_id)
|
||||
.filter((projectId): projectId is string => !!projectId),
|
||||
)
|
||||
const keys = new Set(
|
||||
(content.addons ?? []).map((addon) => addon.project_id ?? addon.filename),
|
||||
)
|
||||
serverContentProjectIds.value = ids
|
||||
serverContentInstallKeys.value = keys
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
@@ -159,6 +340,7 @@ export function createServerInstallContent(opts: {
|
||||
}
|
||||
|
||||
if (resolvedWorldId) {
|
||||
queuedServerInstalls.value = readStoredQueue(sid, resolvedWorldId)
|
||||
await refreshServerInstalledContent(sid, resolvedWorldId)
|
||||
}
|
||||
}
|
||||
@@ -168,11 +350,15 @@ export function createServerInstallContent(opts: {
|
||||
if (!sid) {
|
||||
serverContextServerData.value = null
|
||||
serverContentProjectIds.value = new Set()
|
||||
serverContentInstallKeys.value = new Set()
|
||||
setQueuedServerInstallPlans(new Map())
|
||||
return
|
||||
}
|
||||
|
||||
if (sid !== prevSid) {
|
||||
serverContentProjectIds.value = new Set()
|
||||
serverContentInstallKeys.value = new Set()
|
||||
queuedServerInstalls.value = readStoredQueue(sid, wid)
|
||||
try {
|
||||
serverContextServerData.value = await client.archon.servers_v0.get(sid)
|
||||
} catch (err) {
|
||||
@@ -180,28 +366,16 @@ export function createServerInstallContent(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
if (wid !== prevWid) {
|
||||
queuedServerInstalls.value = readStoredQueue(sid, wid)
|
||||
}
|
||||
|
||||
if (wid && (sid !== prevSid || wid !== prevWid)) {
|
||||
await refreshServerInstalledContent(sid, wid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeLoader(loader: string) {
|
||||
return loader.toLowerCase().replaceAll('_', '').replaceAll('-', '').replaceAll(' ', '')
|
||||
}
|
||||
|
||||
function getCompatibleLoaders(loader: string) {
|
||||
const normalized = normalizeLoader(loader)
|
||||
if (!normalized) return new Set<string>()
|
||||
if (normalized === 'paper' || normalized === 'purpur' || normalized === 'spigot') {
|
||||
return new Set(['paper', 'purpur', 'spigot', 'bukkit'])
|
||||
}
|
||||
if (normalized === 'neoforge' || normalized === 'neo') {
|
||||
return new Set(['neoforge', 'neo'])
|
||||
}
|
||||
return new Set([normalized])
|
||||
}
|
||||
|
||||
function enforceSetupModpackRoute(currentProjectType: string | undefined) {
|
||||
if (!isSetupServerContext.value || currentProjectType === 'modpack') return
|
||||
router.replace({
|
||||
@@ -248,82 +422,132 @@ export function createServerInstallContent(opts: {
|
||||
ctx.modal.value?.setStage('final-config')
|
||||
}
|
||||
|
||||
function getCurrentServerInstallType(): ServerInstallableType {
|
||||
const raw = Array.isArray(route.params.projectType)
|
||||
? route.params.projectType[0]
|
||||
: route.params.projectType
|
||||
if (raw === 'modpack' || raw === 'mod' || raw === 'plugin' || raw === 'datapack') {
|
||||
return raw
|
||||
}
|
||||
throw new Error('This content type cannot be installed to a server from browse.')
|
||||
function clearQueuedServerInstalls() {
|
||||
setQueuedServerInstallPlans(new Map())
|
||||
}
|
||||
|
||||
async function installProjectToServer(project: InstallableSearchResult) {
|
||||
const contentType = getCurrentServerInstallType()
|
||||
const sid = serverIdQuery.value
|
||||
const wid = effectiveServerWorldId.value
|
||||
if (!sid || !wid) {
|
||||
throw new Error('No server world is available for install.')
|
||||
}
|
||||
function removeQueuedServerInstall(projectId: string) {
|
||||
const nextPlans = new Map(queuedServerInstalls.value)
|
||||
nextPlans.delete(projectId)
|
||||
setQueuedServerInstallPlans(nextPlans)
|
||||
}
|
||||
|
||||
if (contentType === 'modpack') {
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
|
||||
include_changelog: false,
|
||||
})
|
||||
const versionId = versions[0]?.id ?? project.version_id
|
||||
if (!versionId) {
|
||||
throw new Error('No version found for this modpack')
|
||||
}
|
||||
async function flushQueuedServerInstalls(
|
||||
serverId: string | null = serverIdQuery.value,
|
||||
worldId: string | null = effectiveServerWorldId.value,
|
||||
) {
|
||||
if (queuedServerInstalls.value.size === 0) return true
|
||||
if (isInstallingQueuedServerInstalls.value) return false
|
||||
|
||||
await openServerModpackInstallFlow({
|
||||
projectId: project.project_id,
|
||||
versionId,
|
||||
name: project.name,
|
||||
iconUrl: project.icon_url ?? undefined,
|
||||
})
|
||||
if (!serverId || !worldId) {
|
||||
handleError(new Error('No server world is available for install.'))
|
||||
return false
|
||||
}
|
||||
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.project_id, {
|
||||
include_changelog: false,
|
||||
})
|
||||
const serverLoader = (serverContextServerData.value?.loader ?? '').toLowerCase()
|
||||
const serverGameVersion = (serverContextServerData.value?.mc_version ?? '').trim()
|
||||
const compatibleLoaders = getCompatibleLoaders(serverLoader)
|
||||
|
||||
const hasGameVersionMatch = (version: Labrinth.Versions.v2.Version) =>
|
||||
!serverGameVersion || version.game_versions.includes(serverGameVersion)
|
||||
const hasLoaderMatch = (version: Labrinth.Versions.v2.Version) => {
|
||||
if (contentType === 'datapack') return true
|
||||
if (compatibleLoaders.size === 0) return true
|
||||
return version.loaders.some((loader) => compatibleLoaders.has(normalizeLoader(loader)))
|
||||
const installedProjectIds = new Set<string>()
|
||||
isInstallingQueuedServerInstalls.value = true
|
||||
queuedInstallProgress.value = {
|
||||
completed: 0,
|
||||
total: queuedServerInstalls.value.size,
|
||||
}
|
||||
|
||||
let matchingVersion = versions.find(
|
||||
(version) => hasGameVersionMatch(version) && hasLoaderMatch(version),
|
||||
)
|
||||
if (!matchingVersion) {
|
||||
matchingVersion = versions.find((version) => hasLoaderMatch(version))
|
||||
try {
|
||||
const result = await flushInstallQueue({
|
||||
queue: {
|
||||
get: () => queuedServerInstalls.value,
|
||||
set: (plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>) => {
|
||||
queuedServerInstalls.value = plans
|
||||
writeStoredQueue(serverId, worldId, plans)
|
||||
},
|
||||
},
|
||||
install: async (plan) => {
|
||||
await client.archon.content_v1.addAddon(serverId, worldId, {
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
})
|
||||
installedProjectIds.add(plan.projectId)
|
||||
},
|
||||
onError: (error, plan) => {
|
||||
removePendingServerContentInstall(serverId, worldId, plan.projectId)
|
||||
handleError(error as Error)
|
||||
},
|
||||
onProgress: (completed, total) => {
|
||||
queuedInstallProgress.value = { completed, total }
|
||||
},
|
||||
})
|
||||
|
||||
if (installedProjectIds.size > 0) {
|
||||
serverContentProjectIds.value = new Set([
|
||||
...serverContentProjectIds.value,
|
||||
...installedProjectIds,
|
||||
])
|
||||
serverContentInstallKeys.value = new Set([
|
||||
...serverContentInstallKeys.value,
|
||||
...installedProjectIds,
|
||||
])
|
||||
}
|
||||
|
||||
return result.ok
|
||||
} finally {
|
||||
isInstallingQueuedServerInstalls.value = false
|
||||
queuedInstallProgress.value = { completed: 0, total: 0 }
|
||||
}
|
||||
if (!matchingVersion) {
|
||||
matchingVersion = versions.find((version) => hasGameVersionMatch(version))
|
||||
}
|
||||
|
||||
async function discardQueuedServerInstallsAndBack() {
|
||||
clearQueuedServerInstalls()
|
||||
await router.push(serverBackUrl.value)
|
||||
}
|
||||
|
||||
async function installQueuedServerInstallsAndBack() {
|
||||
const sid = serverIdQuery.value
|
||||
const wid = effectiveServerWorldId.value
|
||||
const backUrl = serverBackUrl.value
|
||||
const plans = new Map(queuedServerInstalls.value)
|
||||
|
||||
if (sid && wid) {
|
||||
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
|
||||
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
|
||||
void getQueuedInstallPlaceholders(client, plans)
|
||||
.then((items) => {
|
||||
const pendingProjectIds = new Set(
|
||||
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
|
||||
)
|
||||
addPendingServerContentInstalls(
|
||||
sid,
|
||||
wid,
|
||||
items.filter((item) => pendingProjectIds.has(item.projectId)),
|
||||
)
|
||||
})
|
||||
.catch((err) => handleError(err as Error))
|
||||
}
|
||||
if (!matchingVersion) {
|
||||
matchingVersion = versions[0]
|
||||
}
|
||||
if (!matchingVersion) {
|
||||
throw new Error('No installable version was found for this project.')
|
||||
await router.push(backUrl)
|
||||
|
||||
const ok = await flushQueuedServerInstalls(sid, wid)
|
||||
if (!ok) {
|
||||
queuedServerInstalls.value = new Map()
|
||||
writeStoredQueue(sid, wid, new Map())
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Some projects failed to install',
|
||||
text: 'Failed projects were not added. You can try installing them again.',
|
||||
})
|
||||
}
|
||||
|
||||
await client.archon.content_v1.addAddon(sid, wid, {
|
||||
project_id: matchingVersion.project_id,
|
||||
version_id: matchingVersion.id,
|
||||
})
|
||||
|
||||
serverContentProjectIds.value = new Set([...serverContentProjectIds.value, project.project_id])
|
||||
return true
|
||||
}
|
||||
|
||||
function getQueuedServerInstallPlans() {
|
||||
return queuedServerInstalls.value
|
||||
}
|
||||
|
||||
function setQueuedServerInstallPlans(
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
queuedServerInstalls.value = plans
|
||||
writeStoredQueue(serverIdQuery.value, effectiveServerWorldId.value, plans)
|
||||
}
|
||||
|
||||
function onServerFlowBack() {
|
||||
serverSetupModalRef.value?.hide()
|
||||
}
|
||||
@@ -377,15 +601,27 @@ export function createServerInstallContent(opts: {
|
||||
effectiveServerWorldId,
|
||||
serverContextServerData,
|
||||
serverContentProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
selectedServerInstallProjects,
|
||||
isInstallingQueuedServerInstalls,
|
||||
queuedInstallProgress,
|
||||
serverBackUrl,
|
||||
serverBackLabel,
|
||||
serverBrowseHeading,
|
||||
clearQueuedServerInstalls,
|
||||
removeQueuedServerInstall,
|
||||
flushQueuedServerInstalls,
|
||||
discardQueuedServerInstallsAndBack,
|
||||
installQueuedServerInstallsAndBack,
|
||||
initServerContext,
|
||||
watchServerContextChanges,
|
||||
searchServerModpacks,
|
||||
getServerProjectVersions,
|
||||
enforceSetupModpackRoute,
|
||||
installProjectToServer,
|
||||
getQueuedServerInstallPlans,
|
||||
setQueuedServerInstallPlans,
|
||||
openServerModpackInstallFlow,
|
||||
onServerFlowBack,
|
||||
handleServerModpackFlowCreate,
|
||||
markServerProjectInstalled,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const isVersionCompatible = (version, project, instance) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const installVersionDependencies = async (profile, version, onDepInstalling) => {
|
||||
export const installVersionDependencies = async (profile, version, reason, onDepInstalling) => {
|
||||
const projectNames = new Map()
|
||||
const storeProjectName = (p) => {
|
||||
if (p?.id && p.title) projectNames.set(p.id, p.title)
|
||||
@@ -177,7 +177,7 @@ export const installVersionDependencies = async (profile, version, onDepInstalli
|
||||
const batch = queuedInstalls.slice(i, i + batchSize)
|
||||
await Promise.all(
|
||||
batch.map(async ({ versionId }) => {
|
||||
await add_project_from_version(profile.path, versionId, 'dependency')
|
||||
await add_project_from_version(profile.path, versionId, reason)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
+253
-19
@@ -1,4 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{Manager, PhysicalPosition, PhysicalSize, Runtime};
|
||||
@@ -14,6 +16,148 @@ pub struct AdsState {
|
||||
}
|
||||
|
||||
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
const ADS_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36";
|
||||
|
||||
#[cfg(windows)]
|
||||
fn ads_user_agent_override_params() -> String {
|
||||
serde_json::json!({
|
||||
"userAgent": ADS_USER_AGENT,
|
||||
"platform": "Win32",
|
||||
"userAgentMetadata": {
|
||||
"brands": [
|
||||
{ "brand": "Chromium", "version": "128" },
|
||||
{ "brand": "Google Chrome", "version": "128" },
|
||||
{ "brand": "Not=A?Brand", "version": "99" },
|
||||
],
|
||||
"fullVersion": "128.0.0.0",
|
||||
"fullVersionList": [
|
||||
{ "brand": "Chromium", "version": "128.0.0.0" },
|
||||
{ "brand": "Google Chrome", "version": "128.0.0.0" },
|
||||
{ "brand": "Not=A?Brand", "version": "99.0.0.0" },
|
||||
],
|
||||
"platform": "Windows",
|
||||
"platformVersion": "10.0.0",
|
||||
"architecture": "x86",
|
||||
"bitness": "64",
|
||||
"model": "",
|
||||
"mobile": false,
|
||||
},
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn configure_ads_cookie_settings(
|
||||
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
|
||||
) {
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::{
|
||||
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
|
||||
ICoreWebView2_13, ICoreWebView2Profile3,
|
||||
};
|
||||
use windows_core::Interface;
|
||||
|
||||
match core_webview2
|
||||
.cast::<ICoreWebView2_13>()
|
||||
.and_then(|core_webview2| unsafe { core_webview2.Profile() })
|
||||
.and_then(|profile| profile.cast::<ICoreWebView2Profile3>())
|
||||
{
|
||||
Ok(profile) => {
|
||||
if let Err(error) = unsafe {
|
||||
profile.SetPreferredTrackingPreventionLevel(
|
||||
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE,
|
||||
)
|
||||
} {
|
||||
tracing::warn!(
|
||||
?error,
|
||||
"Failed to disable ads WebView2 tracking prevention"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
?error,
|
||||
"Failed to access ads WebView2 profile tracking prevention settings"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_webview_visible<R: Runtime>(
|
||||
webview: &tauri::Webview<R>,
|
||||
_visible: bool,
|
||||
) {
|
||||
webview
|
||||
.with_webview(
|
||||
#[allow(unused_variables)]
|
||||
move |wv| {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let controller = wv.controller();
|
||||
unsafe { controller.SetIsVisible(_visible) }.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn set_webview_visible_for_window<R: Runtime>(
|
||||
app: &tauri::AppHandle<R>,
|
||||
webview: &tauri::Webview<R>,
|
||||
visible: bool,
|
||||
) {
|
||||
let is_minimized = app
|
||||
.get_window("main")
|
||||
.and_then(|window| window.is_minimized().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
set_webview_visible(webview, visible && !is_minimized);
|
||||
}
|
||||
|
||||
fn sync_webview_visibility_for_main_window<R: Runtime>(
|
||||
app: &tauri::AppHandle<R>,
|
||||
main_window: &tauri::Window<R>,
|
||||
was_minimized: &AtomicBool,
|
||||
) {
|
||||
let is_minimized = main_window.is_minimized().unwrap_or(false);
|
||||
let was = was_minimized.load(Ordering::SeqCst);
|
||||
|
||||
if is_minimized == was {
|
||||
return;
|
||||
}
|
||||
|
||||
was_minimized.store(is_minimized, Ordering::SeqCst);
|
||||
|
||||
let ads_visible = if is_minimized {
|
||||
false
|
||||
} else {
|
||||
match app.state::<RwLock<AdsState>>().try_read() {
|
||||
Ok(state) => state.shown && !state.modal_shown,
|
||||
Err(_) => false,
|
||||
}
|
||||
};
|
||||
|
||||
let mut webviews = Vec::new();
|
||||
let mut seen_webviews = HashSet::new();
|
||||
|
||||
for webview in main_window.webviews() {
|
||||
seen_webviews.insert(webview.label().to_string());
|
||||
webviews.push(webview);
|
||||
}
|
||||
|
||||
for webview in app.webviews().into_values() {
|
||||
if seen_webviews.insert(webview.label().to_string()) {
|
||||
webviews.push(webview);
|
||||
}
|
||||
}
|
||||
|
||||
for webview in webviews {
|
||||
let visible =
|
||||
!is_minimized && (webview.label() != "ads-window" || ads_visible);
|
||||
|
||||
set_webview_visible(&webview, visible);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
tauri::plugin::Builder::<R>::new("ads")
|
||||
@@ -30,10 +174,11 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
// visible when we refresh, the Aditude wrapper will not make any ad requests
|
||||
// unless Chromium reports the page as visible. The refresh does not reset the
|
||||
// visibility state.
|
||||
let app = app.clone();
|
||||
let refresh_app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
if let Some(webview) = app.webviews().get_mut("ads-window")
|
||||
if let Some(webview) =
|
||||
refresh_app.webviews().get_mut("ads-window")
|
||||
{
|
||||
let _ = webview.navigate(AD_LINK.parse().unwrap());
|
||||
}
|
||||
@@ -43,6 +188,34 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(main_window) = app.get_window("main") {
|
||||
let app_handle = app.clone();
|
||||
let event_window = main_window.clone();
|
||||
let was_minimized = Arc::new(AtomicBool::new(false));
|
||||
|
||||
main_window.on_window_event(move |_| {
|
||||
sync_webview_visibility_for_main_window(
|
||||
&app_handle,
|
||||
&event_window,
|
||||
&was_minimized,
|
||||
);
|
||||
|
||||
let delayed_app_handle = app_handle.clone();
|
||||
let delayed_event_window = event_window.clone();
|
||||
let delayed_was_minimized = was_minimized.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
sync_webview_visibility_for_main_window(
|
||||
&delayed_app_handle,
|
||||
&delayed_event_window,
|
||||
&delayed_was_minimized,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -103,31 +276,38 @@ pub async fn init_ads_window<R: Runtime>(
|
||||
webview.show().ok();
|
||||
webview.set_position(position).ok();
|
||||
webview.set_size(size).ok();
|
||||
set_webview_visible_for_window(&app, webview, true);
|
||||
} else {
|
||||
webview.hide().ok();
|
||||
webview
|
||||
.set_position(PhysicalPosition::new(-1000, -1000))
|
||||
.ok();
|
||||
set_webview_visible(webview, false);
|
||||
}
|
||||
|
||||
Some(webview.clone())
|
||||
} else if let Some(window) = app.get_window("main") {
|
||||
#[cfg(windows)]
|
||||
let webview_url =
|
||||
WebviewUrl::External("about:blank".parse().unwrap());
|
||||
#[cfg(not(windows))]
|
||||
let webview_url = WebviewUrl::External(AD_LINK.parse().unwrap());
|
||||
|
||||
let webview = window.add_child(
|
||||
tauri::webview::WebviewBuilder::new(
|
||||
"ads-window",
|
||||
WebviewUrl::External(
|
||||
AD_LINK.parse().unwrap(),
|
||||
),
|
||||
)
|
||||
.initialization_script_for_all_frames(include_str!("ads-init.js"))
|
||||
// We use a standard Chrome user agent for compatibility with our ad provider,
|
||||
// since Tauri is not recognized by ad providers by default.
|
||||
// Aditude has separately informed SSPs and IVT vendors that this traffic
|
||||
// originates from a desktop app.
|
||||
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
|
||||
.zoom_hotkeys_enabled(false)
|
||||
.transparent(true)
|
||||
.on_new_window(|_, _| tauri::webview::NewWindowResponse::Deny),
|
||||
tauri::webview::WebviewBuilder::new("ads-window", webview_url)
|
||||
.initialization_script_for_all_frames(include_str!(
|
||||
"ads-init.js"
|
||||
))
|
||||
// We use a standard Chrome user agent for compatibility with our ad provider,
|
||||
// since Tauri is not recognized by ad providers by default.
|
||||
// Aditude has separately informed SSPs and IVT vendors that this traffic
|
||||
// originates from a desktop app.
|
||||
.user_agent(ADS_USER_AGENT)
|
||||
.zoom_hotkeys_enabled(false)
|
||||
.transparent(true)
|
||||
.on_new_window(|_, _| {
|
||||
tauri::webview::NewWindowResponse::Deny
|
||||
}),
|
||||
// set both the `hide`/`show` state and `position`,
|
||||
// to ensure that the webview is actually shown/hidden
|
||||
if state.shown {
|
||||
@@ -140,15 +320,68 @@ pub async fn init_ads_window<R: Runtime>(
|
||||
|
||||
if state.shown {
|
||||
webview.show().ok();
|
||||
set_webview_visible_for_window(&app, &webview, true);
|
||||
} else {
|
||||
webview.hide().ok();
|
||||
set_webview_visible(&webview, false);
|
||||
}
|
||||
|
||||
webview.with_webview(#[allow(unused_variables)] |webview2| {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use webview2_com::CallDevToolsProtocolMethodCompletedHandler;
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2_8;
|
||||
use windows_core::Interface;
|
||||
use windows_core::HSTRING;
|
||||
|
||||
let core_webview2 =
|
||||
unsafe { webview2.controller().CoreWebView2() };
|
||||
|
||||
if let Ok(core_webview2) = core_webview2 {
|
||||
configure_ads_cookie_settings(&core_webview2);
|
||||
|
||||
let navigate_webview = core_webview2.clone();
|
||||
let handler =
|
||||
CallDevToolsProtocolMethodCompletedHandler::create(
|
||||
Box::new(move |result: windows_core::Result<()>, _| {
|
||||
if let Err(error) = result {
|
||||
tracing::error!(
|
||||
?error,
|
||||
"Failed to override ads user-agent client hints"
|
||||
);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
navigate_webview
|
||||
.Navigate(&HSTRING::from(AD_LINK))
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}) as Box<_>,
|
||||
);
|
||||
|
||||
unsafe {
|
||||
if let Err(error) = core_webview2
|
||||
.CallDevToolsProtocolMethod(
|
||||
&HSTRING::from(
|
||||
"Emulation.setUserAgentOverride",
|
||||
),
|
||||
&HSTRING::from(
|
||||
ads_user_agent_override_params(),
|
||||
),
|
||||
&handler,
|
||||
)
|
||||
{
|
||||
tracing::error!(
|
||||
?error,
|
||||
"Failed to install ads user-agent client hints override"
|
||||
);
|
||||
|
||||
core_webview2.Navigate(&HSTRING::from(AD_LINK)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let webview2_controller = webview2.controller();
|
||||
let Ok(webview2_8) = unsafe { webview2_controller.CoreWebView2() }
|
||||
@@ -166,9 +399,9 @@ pub async fn init_ads_window<R: Runtime>(
|
||||
None
|
||||
};
|
||||
|
||||
let Some(webview) = webview.clone() else {
|
||||
if webview.is_none() {
|
||||
return Ok(());
|
||||
};
|
||||
}
|
||||
|
||||
// tauri::async_runtime::spawn(async move {
|
||||
// loop {
|
||||
@@ -249,6 +482,7 @@ pub async fn show_ads_window<R: Runtime>(
|
||||
webview.set_size(size).ok();
|
||||
webview.set_position(position).ok();
|
||||
webview.show().ok();
|
||||
set_webview_visible_for_window(&app, webview, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -30,7 +30,22 @@
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
"signingIdentity": null,
|
||||
"dmg": {
|
||||
"background": "./dmg/dmg-background.png",
|
||||
"windowSize": {
|
||||
"width": 661,
|
||||
"height": 432
|
||||
},
|
||||
"appPosition": {
|
||||
"x": 188,
|
||||
"y": 212
|
||||
},
|
||||
"applicationFolderPosition": {
|
||||
"x": 475,
|
||||
"y": 212
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortDescription": "",
|
||||
"linux": {
|
||||
|
||||
@@ -1,64 +1,8 @@
|
||||
/*
|
||||
Base components
|
||||
*/
|
||||
|
||||
.known-error .multiselect__tags {
|
||||
border-color: var(--color-red) !important;
|
||||
background-color: var(--color-warning-bg) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-warning-text);
|
||||
}
|
||||
}
|
||||
|
||||
.grid-display {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-card-md);
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
|
||||
.grid-display__item {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--size-rounded-card);
|
||||
padding: var(--spacing-card-lg);
|
||||
gap: var(--spacing-card-md);
|
||||
outline: 1px solid transparent;
|
||||
|
||||
.label {
|
||||
color: var(--color-heading);
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--color-text-primary);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.goto-link {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.width-12 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
}
|
||||
|
||||
&.width-16 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Cards and body styling
|
||||
*/
|
||||
.base-card {
|
||||
@extend .padding-lg;
|
||||
|
||||
padding: var(--spacing-card-lg);
|
||||
position: relative;
|
||||
|
||||
background-color: var(--color-raised-bg);
|
||||
@@ -129,73 +73,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.padding-lg {
|
||||
padding: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-bg {
|
||||
padding: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-md {
|
||||
padding: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-sm {
|
||||
padding: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-0 {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.padding-block-lg {
|
||||
padding-block: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-block-bg {
|
||||
padding-block: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-block-md {
|
||||
padding-block: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-block-sm {
|
||||
padding-block: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-block-0 {
|
||||
padding-block: 0;
|
||||
}
|
||||
|
||||
.padding-inline-lg {
|
||||
padding-inline: var(--spacing-card-lg);
|
||||
}
|
||||
|
||||
.padding-inline-bg {
|
||||
padding-inline: var(--spacing-card-bg);
|
||||
}
|
||||
|
||||
.padding-inline-md {
|
||||
padding-inline: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
.padding-inline-sm {
|
||||
padding-inline: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.padding-inline-0 {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.universal-body {
|
||||
@extend .universal-labels;
|
||||
|
||||
.multiselect {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
> :where(input + *, .input-group + *, .chips + *, .input-div + *) {
|
||||
&:not(:empty) {
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
@@ -340,19 +220,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-card {
|
||||
@extend .base-card;
|
||||
@extend .padding-inline-lg;
|
||||
@extend .padding-block-md;
|
||||
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.5rem;
|
||||
min-height: 3.75rem;
|
||||
}
|
||||
|
||||
/*
|
||||
Other
|
||||
*/
|
||||
@@ -372,19 +239,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.title-link {
|
||||
text-decoration: underline;
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.button-base {
|
||||
@extend .button-animation;
|
||||
font-weight: 500;
|
||||
@@ -455,36 +309,6 @@ tr.button-transparent {
|
||||
}
|
||||
}
|
||||
|
||||
.button-within {
|
||||
transition:
|
||||
opacity 0.5s ease-in-out,
|
||||
filter 0.2s ease-in-out,
|
||||
transform 0.05s ease-in-out,
|
||||
outline 0.2s ease-in-out;
|
||||
|
||||
&:focus-visible:not(&.disabled),
|
||||
&:hover:not(&.disabled) {
|
||||
filter: brightness(0.85);
|
||||
}
|
||||
|
||||
&:active:not(&.disabled) {
|
||||
filter: brightness(0.8);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
|
||||
&disabled,
|
||||
&[disabled='true'] {
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-color-base {
|
||||
box-sizing: border-box;
|
||||
--text-color: var(--color-button-text);
|
||||
@@ -536,10 +360,6 @@ tr.button-transparent {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.bold-button {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.square-button {
|
||||
@@ -570,24 +390,6 @@ tr.button-transparent {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
@extend .iconified-button;
|
||||
|
||||
box-sizing: content-box;
|
||||
min-height: unset;
|
||||
border-radius: var(--size-rounded-max);
|
||||
white-space: nowrap;
|
||||
padding: 0.5rem 0.75rem;
|
||||
max-height: 1.75rem;
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5rem;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.raised-button {
|
||||
--background-color: var(--color-raised-bg);
|
||||
box-shadow: var(--shadow-inset-sm), var(--shadow-raised);
|
||||
@@ -608,11 +410,6 @@ tr.button-transparent {
|
||||
--text-color: var(--color-brand-inverted);
|
||||
}
|
||||
|
||||
.alt-brand-button {
|
||||
--background-color: var(--color-brand-highlight);
|
||||
--text-color: var(--color-text);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
grid-gap: var(--spacing-card-sm);
|
||||
@@ -621,21 +418,6 @@ tr.button-transparent {
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.multiselect--above .multiselect__content-wrapper {
|
||||
border-top: none !important;
|
||||
border-top-left-radius: var(--size-rounded-card) !important;
|
||||
border-top-right-radius: var(--size-rounded-card) !important;
|
||||
}
|
||||
|
||||
.known-error .multiselect__tags {
|
||||
border-color: var(--color-red) !important;
|
||||
background-color: var(--color-warning-bg) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-warning-text);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -707,36 +489,6 @@ textarea.known-error {
|
||||
color: var(--color-link-active);
|
||||
}
|
||||
|
||||
h1 {
|
||||
.beta-badge {
|
||||
font-size: 0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-badge {
|
||||
font-size: 0.7em;
|
||||
padding: 0.2rem 0.4rem;
|
||||
background-color: transparent;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--color-text);
|
||||
color: var(--color-text);
|
||||
border-radius: var(--size-rounded-max);
|
||||
margin-left: 0.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.router-link-exact-active,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
.beta-badge {
|
||||
background-color: var(--color-button-text-active);
|
||||
box-sizing: border-box;
|
||||
border-color: transparent;
|
||||
color: var(--color-raised-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.button-animation,
|
||||
button {
|
||||
@@ -751,7 +503,6 @@ h3 {
|
||||
}
|
||||
|
||||
.full-width-inputs {
|
||||
.multiselect,
|
||||
input,
|
||||
.iconified-input {
|
||||
width: 100%;
|
||||
@@ -774,10 +525,6 @@ button {
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
|
||||
.multiselect {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
input {
|
||||
flex-shrink: 2;
|
||||
}
|
||||
@@ -799,20 +546,6 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.input-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-bottom: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
> .multiselect {
|
||||
width: unset;
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -844,22 +577,6 @@ button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.text-input-wrapper__after {
|
||||
display: flex;
|
||||
color: var(--color-text);
|
||||
padding: 0.5rem 1rem 0.5rem 0;
|
||||
font-weight: var(--font-weight-medium);
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
filter: grayscale(50%);
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border-radius: 0;
|
||||
@@ -880,30 +597,6 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.primary-stat {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 0.6rem;
|
||||
|
||||
.primary-stat__icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.primary-stat__text {
|
||||
margin-left: 0.4rem;
|
||||
}
|
||||
|
||||
.primary-stat__counter {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.project-list {
|
||||
width: 100%;
|
||||
gap: var(--spacing-card-md);
|
||||
@@ -1033,181 +726,87 @@ a.iconified-link {
|
||||
}
|
||||
}
|
||||
|
||||
a.subtle-link {
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
filter: var(--hover-filter);
|
||||
}
|
||||
|
||||
&:active {
|
||||
filter: var(--active-filter);
|
||||
}
|
||||
}
|
||||
|
||||
.inline-svg svg,
|
||||
svg.inline-svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// START STUFF FOR OMORPHIA
|
||||
.experimental-styles-within {
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-4);
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-4);
|
||||
}
|
||||
|
||||
.flex-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-12);
|
||||
padding: var(--gap-16);
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-18);
|
||||
font-weight: var(--weight-extrabold);
|
||||
color: var(--color-contrast);
|
||||
line-height: initial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag-list__item {
|
||||
background-color: var(--_bg-color, var(--color-button-bg));
|
||||
padding: var(--gap-4) var(--gap-8);
|
||||
border-radius: var(--radius-max);
|
||||
h3 {
|
||||
font-size: var(--text-16);
|
||||
font-weight: var(--weight-bold);
|
||||
font-size: var(--text-14);
|
||||
display: flex;
|
||||
gap: var(--gap-4);
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
color: var(--_color, var(--color-secondary));
|
||||
|
||||
svg {
|
||||
width: var(--icon-14);
|
||||
height: var(--icon-14);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-8);
|
||||
padding-left: var(--gap-6);
|
||||
|
||||
color: var(--color-base);
|
||||
font-weight: var(--weight-bold);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--gap-4);
|
||||
|
||||
svg {
|
||||
width: var(--icon-16);
|
||||
height: var(--icon-16);
|
||||
margin-right: var(--gap-4);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--color-secondary);
|
||||
font-style: italic;
|
||||
font-weight: var(--weight-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.status-list__item--color-green svg {
|
||||
color: var(--color-green);
|
||||
}
|
||||
|
||||
.status-list__item--color-orange svg {
|
||||
color: var(--color-orange);
|
||||
}
|
||||
|
||||
.status-list__item--color-red svg {
|
||||
color: var(--color-red);
|
||||
}
|
||||
|
||||
.status-list__item--color-blue svg {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
|
||||
.status-list__item--color-purple svg {
|
||||
color: var(--color-purple);
|
||||
}
|
||||
|
||||
&.flex-card,
|
||||
.flex-card {
|
||||
> section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-12);
|
||||
padding: var(--gap-16);
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-18);
|
||||
font-weight: var(--weight-extrabold);
|
||||
color: var(--color-contrast);
|
||||
line-height: initial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-16);
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--color-base);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-8);
|
||||
}
|
||||
}
|
||||
|
||||
.list-style {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-12);
|
||||
font-weight: var(--weight-bold);
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
border-color: var(--color-button-border);
|
||||
margin-block: var(--gap-2);
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-list-item {
|
||||
display: flex;
|
||||
gap: var(--gap-8);
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: var(--icon-16);
|
||||
height: var(--icon-16);
|
||||
}
|
||||
.list-style {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-12);
|
||||
font-weight: var(--weight-bold);
|
||||
|
||||
> svg:first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
hr {
|
||||
width: 100%;
|
||||
border-color: var(--color-button-border);
|
||||
margin-block: var(--gap-2);
|
||||
}
|
||||
}
|
||||
|
||||
.iconified-list-item {
|
||||
display: flex;
|
||||
gap: var(--gap-8);
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
|
||||
svg {
|
||||
width: var(--icon-16);
|
||||
height: var(--icon-16);
|
||||
}
|
||||
|
||||
.links-list {
|
||||
@extend .list-style;
|
||||
|
||||
> a {
|
||||
@extend .iconified-list-item;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
> svg:first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.details-list {
|
||||
@extend .list-style;
|
||||
}
|
||||
.details-list {
|
||||
@extend .list-style;
|
||||
}
|
||||
|
||||
.details-list__item {
|
||||
@extend .iconified-list-item;
|
||||
.details-list__item {
|
||||
@extend .iconified-list-item;
|
||||
|
||||
.details-list__item__text--style-secondary {
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--weight-normal);
|
||||
font-size: var(--text-14);
|
||||
}
|
||||
.details-list__item__text--style-secondary {
|
||||
color: var(--color-secondary);
|
||||
font-weight: var(--weight-normal);
|
||||
font-size: var(--text-14);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,74 +17,60 @@
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fileIsValid } from '~/helpers/fileUtils.js'
|
||||
<script setup lang="ts">
|
||||
import { useFormatBytes } from '@modrinth/ui'
|
||||
import { fileIsValid } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
prompt: {
|
||||
type: String,
|
||||
default: 'Select file',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
prompt?: string
|
||||
multiple?: boolean
|
||||
accept?: string
|
||||
/**
|
||||
* The max file size in bytes
|
||||
*/
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
shouldAlwaysReset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
longStyle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxSize?: number | null
|
||||
showIcon?: boolean
|
||||
shouldAlwaysReset?: boolean
|
||||
longStyle?: boolean
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
prompt: 'Select file',
|
||||
multiple: false,
|
||||
showIcon: true,
|
||||
shouldAlwaysReset: false,
|
||||
longStyle: false,
|
||||
disabled: false,
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addFiles(files, shouldNotReset) {
|
||||
if (!shouldNotReset || this.shouldAlwaysReset) {
|
||||
this.files = files
|
||||
}
|
||||
)
|
||||
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
|
||||
const emit = defineEmits<{ change: [files: File[]] }>()
|
||||
|
||||
if (this.files.length > 0) {
|
||||
this.$emit('change', this.files)
|
||||
}
|
||||
},
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const files = ref<File[]>([])
|
||||
|
||||
function addFiles(incoming: FileList, shouldNotReset = false) {
|
||||
if (!shouldNotReset || props.shouldAlwaysReset) {
|
||||
files.value = Array.from(incoming)
|
||||
}
|
||||
const validationOptions = { maxSize: props.maxSize, alertOnInvalid: true }
|
||||
files.value = files.value.filter((file) => fileIsValid(file, validationOptions, formatBytes))
|
||||
if (files.value.length > 0) {
|
||||
emit('change', files.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
addFiles(e.dataTransfer!.files)
|
||||
}
|
||||
|
||||
function handleChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (!input.files) return
|
||||
addFiles(input.files)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
<div class="modal-body">
|
||||
<div v-if="header" class="header">
|
||||
<strong>{{ header }}</strong>
|
||||
<button class="iconified-button icon-only transparent" @click="hide">
|
||||
<XIcon />
|
||||
</button>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
@@ -27,9 +29,11 @@
|
||||
|
||||
<script>
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ButtonStyled,
|
||||
XIcon,
|
||||
},
|
||||
props: {
|
||||
|
||||
@@ -223,9 +223,7 @@ function developerModeIncrement() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer
|
||||
class="footer-brand-background experimental-styles-within border-0 border-t-[1px] border-solid"
|
||||
>
|
||||
<footer class="footer-brand-background border-0 border-t-[1px] border-solid">
|
||||
<div class="mx-auto flex max-w-screen-xl flex-col gap-6 p-6 pb-20 sm:px-12 md:py-12">
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 text-primary md:grid-cols-[1fr_2fr] lg:grid-cols-[auto_auto_auto_auto_auto]"
|
||||
|
||||
@@ -199,47 +199,9 @@
|
||||
</span>
|
||||
<div v-if="compact" class="notification__actions">
|
||||
<template v-if="type === 'team_invite' || type === 'organization_invite'">
|
||||
<button
|
||||
v-tooltip="`Accept`"
|
||||
class="iconified-button square-button brand-button button-transparent"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
</button>
|
||||
<button
|
||||
v-tooltip="`Decline`"
|
||||
class="iconified-button square-button danger-button button-transparent"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-else-if="!notification.read"
|
||||
v-tooltip="`Mark as read`"
|
||||
class="iconified-button square-button button-transparent"
|
||||
@click="read()"
|
||||
>
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="notification__actions">
|
||||
<div v-if="type !== null" class="input-group">
|
||||
<template
|
||||
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
|
||||
>
|
||||
<ButtonStyled circular color="brand" type="transparent">
|
||||
<button
|
||||
class="iconified-button brand-button"
|
||||
v-tooltip="`Accept`"
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
@@ -248,10 +210,11 @@
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular color="red" type="transparent">
|
||||
<button
|
||||
class="iconified-button danger-button"
|
||||
v-tooltip="`Decline`"
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
@@ -260,51 +223,75 @@
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
Decline
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
v-else-if="!notification.read"
|
||||
class="iconified-button"
|
||||
:class="{ 'raised-button': raised }"
|
||||
@click="read()"
|
||||
>
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else-if="!notification.read" circular type="transparent">
|
||||
<button v-tooltip="`Mark as read`" @click="read()">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="notification__actions">
|
||||
<div v-if="type !== null" class="input-group">
|
||||
<template
|
||||
v-if="(type === 'team_invite' || type === 'organization_invite') && !notification.read"
|
||||
>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
@click="
|
||||
() => {
|
||||
acceptTeamInvite(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<CheckIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
@click="
|
||||
() => {
|
||||
removeSelfFromTeam(notification.body.team_id)
|
||||
read()
|
||||
}
|
||||
"
|
||||
>
|
||||
<XIcon />
|
||||
Decline
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else-if="!notification.read">
|
||||
<button @click="read()">
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
<div v-else class="input-group">
|
||||
<nuxt-link
|
||||
v-if="notification.link && notification.link !== '#'"
|
||||
class="iconified-button"
|
||||
:class="{ 'raised-button': raised }"
|
||||
:to="notification.link"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Open link
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-for="(action, actionIndex) in notification.actions"
|
||||
:key="actionIndex"
|
||||
class="iconified-button"
|
||||
:class="{ 'raised-button': raised }"
|
||||
@click="performAction(notification, actionIndex)"
|
||||
>
|
||||
<CheckIcon v-if="action.title === 'Accept'" />
|
||||
<XIcon v-else-if="action.title === 'Deny'" />
|
||||
{{ action.title }}
|
||||
</button>
|
||||
<button
|
||||
v-if="notification.actions.length === 0 && !notification.read"
|
||||
class="iconified-button"
|
||||
:class="{ 'raised-button': raised }"
|
||||
@click="performAction(notification, null)"
|
||||
>
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
</button>
|
||||
<ButtonStyled v-if="notification.link && notification.link !== '#'">
|
||||
<nuxt-link :to="notification.link" target="_blank">
|
||||
<ExternalIcon />
|
||||
Open link
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-for="(action, actionIndex) in notification.actions" :key="actionIndex">
|
||||
<button @click="performAction(notification, actionIndex)">
|
||||
<CheckIcon v-if="action.title === 'Accept'" />
|
||||
<XIcon v-else-if="action.title === 'Deny'" />
|
||||
{{ action.title }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="notification.actions.length === 0 && !notification.read">
|
||||
<button @click="performAction(notification, null)">
|
||||
<CheckIcon />
|
||||
Mark as read
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<CopyCode v-if="flags.developerMode" :text="notification.id" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -325,6 +312,7 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Categories,
|
||||
CopyCode,
|
||||
DoubleIcon,
|
||||
@@ -594,10 +582,6 @@ function getLoaderCategories(ver) {
|
||||
gap: var(--spacing-card-sm);
|
||||
}
|
||||
|
||||
.notification__actions .iconified-button.square-button svg {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.unknown-type {
|
||||
color: var(--color-red);
|
||||
}
|
||||
@@ -618,4 +602,8 @@ function getLoaderCategories(ver) {
|
||||
color: var(--color-blue);
|
||||
}
|
||||
}
|
||||
|
||||
.title-link {
|
||||
@apply underline hover:brightness-[--hover-brightness];
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<nav
|
||||
ref="scrollContainer"
|
||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
class="card-shadow relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||
>
|
||||
<button
|
||||
v-for="(option, index) in options"
|
||||
|
||||
@@ -67,50 +67,55 @@
|
||||
</div>
|
||||
|
||||
<div class="table-cell">
|
||||
<nuxt-link
|
||||
class="btn icon-only"
|
||||
:to="`/project/${project.slug ? project.slug : project.id}/settings`"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link :to="`/project/${project.slug ? project.slug : project.id}/settings`">
|
||||
<SettingsIcon />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="push-right input-group">
|
||||
<Button @click="$refs.modalOpen?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button :disabled="!selectedProjects?.length" color="primary" @click="onSubmitHandler()">
|
||||
<TransferIcon />
|
||||
<span>
|
||||
Transfer
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="$refs.modalOpen?.hide()">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!selectedProjects?.length" @click="onSubmitHandler()">
|
||||
<TransferIcon />
|
||||
<span>
|
||||
{{
|
||||
selectedProjects.length === props.projects.length
|
||||
? 'All'
|
||||
: selectedProjects.length
|
||||
}}
|
||||
Transfer
|
||||
<span>
|
||||
{{
|
||||
selectedProjects.length === props.projects.length
|
||||
? 'All'
|
||||
: selectedProjects.length
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ ' ' }}
|
||||
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
{{ ' ' }}
|
||||
{{ selectedProjects.length === 1 ? 'project' : 'projects' }}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Button @click="$refs.modalOpen?.show()">
|
||||
<TransferIcon />
|
||||
<span>Transfer projects</span>
|
||||
</Button>
|
||||
<ButtonStyled>
|
||||
<button @click="$refs.modalOpen?.show()">
|
||||
<TransferIcon />
|
||||
<span>Transfer projects</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Checkbox, CopyCode, Modal } from '@modrinth/ui'
|
||||
import { Avatar, ButtonStyled, Checkbox, CopyCode, Modal } from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { SettingsIcon } from '@modrinth/assets'
|
||||
import { defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
|
||||
import { ButtonStyled, defineMessages, PagewideBanner, useVIntl } from '@modrinth/ui'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -30,10 +30,12 @@ const messages = defineMessages({
|
||||
<span>{{ formatMessage(messages.description) }}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<nuxt-link class="btn" to="/settings/billing">
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.action) }}
|
||||
</nuxt-link>
|
||||
<ButtonStyled>
|
||||
<nuxt-link to="/settings/billing">
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.action) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PagewideBanner>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { SettingsIcon } from '@modrinth/assets'
|
||||
import { defineMessages, injectNotificationManager, PagewideBanner, useVIntl } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
PagewideBanner,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { FetchError } from 'ofetch'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -91,13 +97,17 @@ async function handleResendEmailVerification() {
|
||||
</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<button v-if="hasEmail" class="btn" @click="handleResendEmailVerification">
|
||||
{{ formatMessage(verifyEmailBannerMessages.action) }}
|
||||
</button>
|
||||
<nuxt-link v-else class="btn" to="/settings/account">
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
{{ formatMessage(addEmailBannerMessages.action) }}
|
||||
</nuxt-link>
|
||||
<ButtonStyled v-if="hasEmail">
|
||||
<button @click="handleResendEmailVerification">
|
||||
{{ formatMessage(verifyEmailBannerMessages.action) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<nuxt-link to="/settings/account">
|
||||
<SettingsIcon aria-hidden="true" />
|
||||
{{ formatMessage(addEmailBannerMessages.action) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PagewideBanner>
|
||||
</template>
|
||||
|
||||
@@ -81,15 +81,21 @@
|
||||
</span>
|
||||
</h2>
|
||||
<div class="chart-controls__buttons">
|
||||
<Button v-tooltip="'Toggle project colors'" icon-only @click="onToggleColors">
|
||||
<PaletteIcon />
|
||||
</Button>
|
||||
<Button v-tooltip="'Download this data as CSV'" icon-only @click="onDownloadSetAsCSV">
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
<Button v-tooltip="'Refresh the chart'" icon-only @click="resetCharts">
|
||||
<UpdatedIcon />
|
||||
</Button>
|
||||
<ButtonStyled circular type="outlined">
|
||||
<button v-tooltip="'Toggle project colors'" @click="onToggleColors">
|
||||
<PaletteIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="outlined">
|
||||
<button v-tooltip="'Download this data as CSV'" @click="onDownloadSetAsCSV">
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="outlined">
|
||||
<button v-tooltip="'Refresh the chart'" @click="resetCharts">
|
||||
<UpdatedIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<DropdownSelect
|
||||
v-model="selectedRange"
|
||||
class="range-dropdown"
|
||||
@@ -156,8 +162,8 @@
|
||||
<template v-for="project in selectedDataSetProjects" :key="project">
|
||||
<button
|
||||
v-tooltip="project.title"
|
||||
:class="`legend__item button-base btn-transparent ${
|
||||
!projectIsOnDisplay(project.id) ? 'btn-dimmed' : ''
|
||||
:class="`legend__item button-base legend__item-transparent ${
|
||||
!projectIsOnDisplay(project.id) ? 'legend__item-dimmed' : ''
|
||||
}`"
|
||||
@click="
|
||||
() =>
|
||||
@@ -311,7 +317,7 @@
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, PaletteIcon, UpdatedIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
Card,
|
||||
DropdownSelect,
|
||||
useCompactNumber,
|
||||
@@ -842,7 +848,7 @@ const defaultRanges: RangeObject[] = [
|
||||
}
|
||||
}
|
||||
|
||||
.btn-transparent {
|
||||
.legend__item-transparent {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
@@ -851,7 +857,7 @@ const defaultRanges: RangeObject[] = [
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
.btn-dimmed {
|
||||
.legend__item-dimmed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
{{ formatMessage(messages.collectionInfo, { count: projectIds.length }) }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled class="w-24">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="modal.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" class="w-36">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="hasHitLimit" @click="create">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createCollection) }}
|
||||
|
||||
@@ -59,13 +59,13 @@
|
||||
{{ formatMessage(messages.ownershipInfo) }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled class="w-24">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" class="w-40">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="hasHitLimit" @click="createOrganization">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createOrganization) }}
|
||||
|
||||
@@ -110,13 +110,13 @@
|
||||
<span>{{ formatMessage(messages.summaryDescription) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2.5">
|
||||
<ButtonStyled class="w-24">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="cancel">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" class="w-32">
|
||||
<ButtonStyled color="brand">
|
||||
<button v-tooltip="missingFieldsTooltip" :disabled="disableCreate" @click="createProject">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createProject) }}
|
||||
|
||||
@@ -58,24 +58,20 @@
|
||||
</div>
|
||||
<template #actions>
|
||||
<div v-if="currentStage === 'completion'" class="mt-4 flex w-full gap-3">
|
||||
<ButtonStyled class="flex-1">
|
||||
<button class="w-full text-contrast" @click="handleClose">
|
||||
<ButtonStyled>
|
||||
<button class="w-full flex-1 text-contrast" @click="handleClose">
|
||||
{{ formatMessage(commonMessages.closeButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled class="flex-1">
|
||||
<button class="w-full text-contrast" @click="handleViewTransactions">
|
||||
<ButtonStyled>
|
||||
<button class="w-full flex-1 text-contrast" @click="handleViewTransactions">
|
||||
{{ formatMessage(messages.transactionsButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-else class="mt-4 flex flex-col justify-end gap-2 sm:flex-row">
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="!border-surface-5"
|
||||
:disabled="leftButtonConfig.disabled"
|
||||
@click="leftButtonConfig.handler"
|
||||
>
|
||||
<button :disabled="leftButtonConfig.disabled" @click="leftButtonConfig.handler">
|
||||
<component :is="leftButtonConfig.icon" />
|
||||
{{ leftButtonConfig.label }}
|
||||
</button>
|
||||
|
||||
@@ -30,9 +30,13 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="grid-display width-16 mt-4">
|
||||
<div v-for="nag in visibleNags" :key="nag.id" class="grid-display__item">
|
||||
<span class="flex items-center gap-2 font-semibold">
|
||||
<div v-if="!collapsed" class="mt-4 grid grid-cols-[repeat(auto-fit,minmax(15rem,1fr))] gap-2">
|
||||
<div
|
||||
v-for="nag in visibleNags"
|
||||
:key="nag.id"
|
||||
class="flex flex-col gap-3 rounded-2xl border border-solid border-surface-5 bg-surface-2 p-4"
|
||||
>
|
||||
<span class="flex items-center gap-2 font-medium text-contrast">
|
||||
<component
|
||||
:is="nag.icon || getDefaultIcon(nag.status)"
|
||||
v-tooltip="getStatusTooltip(nag.status)"
|
||||
@@ -52,7 +56,7 @@
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
||||
nag.link.path
|
||||
}`"
|
||||
class="goto-link"
|
||||
class="goto-link mt-auto"
|
||||
>
|
||||
{{ getFormattedMessage(nag.link.title) }}
|
||||
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
<template>
|
||||
<div class="shadow-card rounded-2xl border border-surface-5 bg-surface-3 p-4">
|
||||
<div class="shadow-card rounded-2xl border border-solid border-surface-5 bg-surface-3 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<Avatar
|
||||
:src="queueEntry.project.icon_url"
|
||||
size="4rem"
|
||||
class="rounded-2xl border border-surface-5 bg-surface-4 !shadow-none"
|
||||
/>
|
||||
<NuxtLink
|
||||
:to="`/project/${queueEntry.project.slug}`"
|
||||
target="_blank"
|
||||
tabindex="-1"
|
||||
class="flex"
|
||||
>
|
||||
<Avatar
|
||||
:src="queueEntry.project.icon_url"
|
||||
size="4rem"
|
||||
class="rounded-2xl border border-surface-5 bg-surface-4 !shadow-none"
|
||||
/>
|
||||
</NuxtLink>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<NuxtLink
|
||||
@@ -22,11 +29,15 @@
|
||||
<component
|
||||
:is="getProjectTypeIcon(queueEntry.project.project_types[0] as any)"
|
||||
aria-hidden="true"
|
||||
class="h-4 w-4"
|
||||
class="size-4"
|
||||
/>
|
||||
<span class="text-sm font-medium text-secondary">
|
||||
{{
|
||||
queueEntry.project.project_types.map((t) => formatProjectType(t, true)).join(', ')
|
||||
queueEntry.project.project_types.length === 0
|
||||
? '???'
|
||||
: queueEntry.project.project_types
|
||||
.map((t) => formatProjectType(t, true))
|
||||
.join(', ')
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
@@ -35,37 +46,39 @@
|
||||
class="flex items-center gap-2 rounded-full border border-solid border-surface-5 bg-surface-4 px-2.5 py-1"
|
||||
>
|
||||
<span class="text-sm text-secondary">Requesting</span>
|
||||
<Badge :type="queueEntry.project.requested_status" class="status" />
|
||||
<Badge :type="queueEntry.project.requested_status" class="text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="queueEntry.owner" class="flex items-center gap-1">
|
||||
<Avatar
|
||||
:src="queueEntry.owner.user.avatar_url"
|
||||
size="1.5rem"
|
||||
circle
|
||||
class="border border-surface-5 bg-surface-4 !shadow-none"
|
||||
/>
|
||||
<div v-if="queueEntry.ownership?.kind === 'user'">
|
||||
<NuxtLink
|
||||
:to="`/user/${queueEntry.owner.user.username}`"
|
||||
:to="`/user/${queueEntry.ownership.id}`"
|
||||
target="_blank"
|
||||
class="text-sm font-medium text-secondary hover:underline"
|
||||
class="flex w-fit min-w-40 items-center gap-1 text-sm font-medium text-secondary hover:underline"
|
||||
>
|
||||
{{ queueEntry.owner.user.username }}
|
||||
<Avatar
|
||||
:src="queueEntry.ownership.icon_url"
|
||||
size="1.5rem"
|
||||
circle
|
||||
class="border border-surface-5 bg-surface-4 !shadow-none"
|
||||
/>
|
||||
{{ queueEntry.ownership.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div v-else-if="queueEntry.org" class="flex items-center gap-1">
|
||||
<div
|
||||
v-else-if="queueEntry.ownership?.kind === 'organization'"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<Avatar
|
||||
:src="queueEntry.org.icon_url"
|
||||
:src="queueEntry.ownership.icon_url"
|
||||
size="1.5rem"
|
||||
circle
|
||||
class="border border-surface-5 bg-surface-4 !shadow-none"
|
||||
/>
|
||||
<NuxtLink
|
||||
:to="`/organization/${queueEntry.org.slug}`"
|
||||
:to="`/organization/${queueEntry.ownership.id}`"
|
||||
target="_blank"
|
||||
class="text-sm font-medium text-secondary hover:underline"
|
||||
>
|
||||
{{ queueEntry.org.name }}
|
||||
{{ queueEntry.ownership.name }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,25 +97,20 @@
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled circular color="orange">
|
||||
<button @click="openProjectForReview">
|
||||
<ScaleIcon class="size-5" />
|
||||
<ButtonStyled circular>
|
||||
<button v-tooltip="'Copy ID'" @click="copyId">
|
||||
<ClipboardCopyIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<OverflowMenu :options="quickActions" :dropdown-id="`${baseId}-quick-actions`">
|
||||
<template #default>
|
||||
<EllipsisVerticalIcon class="size-4" />
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon />
|
||||
<span class="hidden sm:inline">Copy ID</span>
|
||||
</template>
|
||||
<template #copy-link>
|
||||
<LinkIcon />
|
||||
<span class="hidden sm:inline">Copy link</span>
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
<button v-tooltip="'Copy link'" @click="copyLink">
|
||||
<LinkIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-tooltip="'Begin review'" circular color="orange">
|
||||
<button @click="openProjectForReview">
|
||||
<ScaleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,15 +119,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ClipboardCopyIcon, EllipsisVerticalIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
|
||||
import { ClipboardCopyIcon, LinkIcon, ScaleIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
ButtonStyled,
|
||||
getProjectTypeIcon,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
} from '@modrinth/ui'
|
||||
@@ -143,8 +149,6 @@ const formatDateTimeFull = useFormatDateTime({
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
|
||||
const baseId = useId()
|
||||
|
||||
const props = defineProps<{
|
||||
queueEntry: ModerationProject
|
||||
}>()
|
||||
@@ -185,34 +189,27 @@ const formattedDate = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const quickActions: OverflowMenuOption[] = [
|
||||
{
|
||||
id: 'copy-link',
|
||||
action: () => {
|
||||
const base = window.location.origin
|
||||
const projectUrl = `${base}/project/${props.queueEntry.project.slug}`
|
||||
navigator.clipboard.writeText(projectUrl).then(() => {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Project link copied',
|
||||
text: 'The link to this project has been copied to your clipboard.',
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
action: () => {
|
||||
navigator.clipboard.writeText(props.queueEntry.project.id).then(() => {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Project ID copied',
|
||||
text: 'The ID of this project has been copied to your clipboard.',
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
function copyLink() {
|
||||
const base = window.location.origin
|
||||
const projectUrl = `${base}/project/${props.queueEntry.project.slug}`
|
||||
navigator.clipboard.writeText(projectUrl).then(() => {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Project link copied',
|
||||
text: 'The link to this project has been copied to your clipboard.',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function copyId() {
|
||||
navigator.clipboard.writeText(props.queueEntry.project.id).then(() => {
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Project ID copied',
|
||||
text: 'The ID of this project has been copied to your clipboard.',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function openProjectForReview() {
|
||||
emit('startFromProject', props.queueEntry.project.id)
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
@update-thread="updateThread"
|
||||
>
|
||||
<template #closedActions>
|
||||
<ButtonStyled v-if="isStaff(auth.user)" color="green" class="mt-2">
|
||||
<button class="w-full gap-2 sm:w-auto" @click="reopenReport()">
|
||||
<ButtonStyled v-if="isStaff(auth.user)" color="green">
|
||||
<button class="mt-2 w-full gap-2 sm:w-auto" @click="reopenReport()">
|
||||
<CheckCircleIcon class="size-4" />
|
||||
Reopen Thread
|
||||
</button>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
type OverflowMenuOption,
|
||||
useFormatBytes,
|
||||
useFormatDateTime,
|
||||
} from '@modrinth/ui'
|
||||
import { NavTabs } from '@modrinth/ui'
|
||||
@@ -56,6 +57,7 @@ const formatDateTimeUtc = useFormatDateTime({
|
||||
timeZoneName: 'short',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
type FlattenedFileReport = Labrinth.TechReview.Internal.FileReport & {
|
||||
id: string
|
||||
@@ -362,12 +364,6 @@ const formattedDate = computed(() => {
|
||||
return `${diffDays} days ago`
|
||||
})
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KiB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`
|
||||
}
|
||||
|
||||
function viewFileFlags(file: FlattenedFileReport) {
|
||||
selectedFileId.value = file.id
|
||||
currentTab.value = 'File'
|
||||
@@ -851,7 +847,7 @@ const reviewSummaryPreview = computed(() => {
|
||||
const fileVerdict = fileUnsafe > 0 ? 'Unsafe' : 'Safe'
|
||||
|
||||
markdown += `### ${fileData.fileName}\n`
|
||||
markdown += `> ${formatFileSize(fileData.fileSize)} • ${fileData.decisions.length} issues • Max severity: ${fileData.maxSeverity} • **Verdict:** ${fileVerdict}\n\n`
|
||||
markdown += `> ${formatBytes(fileData.fileSize)} • ${fileData.decisions.length} issues • Max severity: ${fileData.maxSeverity} • **Verdict:** ${fileVerdict}\n\n`
|
||||
markdown += `<details>\n<summary>Issues (${fileSafe} safe, ${fileUnsafe} unsafe)</summary>\n\n`
|
||||
markdown += `| Class | Issue Type | Severity | Decision |\n`
|
||||
markdown += `|-------|------------|----------|----------|\n`
|
||||
@@ -1150,7 +1146,7 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
</span>
|
||||
<div class="rounded-full border border-solid border-surface-5 bg-surface-3 px-2.5 py-1">
|
||||
<span class="text-sm font-medium text-secondary">{{
|
||||
formatFileSize(file.file_size)
|
||||
formatBytes(file.file_size)
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -1201,7 +1197,6 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
:href="file.download_url"
|
||||
:title="`Download ${file.file_name}`"
|
||||
:download="file.file_name"
|
||||
class="!border-px !border-surface-4"
|
||||
tabindex="0"
|
||||
>
|
||||
<DownloadIcon /> Download
|
||||
@@ -1362,7 +1357,6 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="!border-[1px]"
|
||||
:disabled="updatingDetails.has(flag.detail.id)"
|
||||
@click="updateDetailStatus(flag.detail.id, 'safe')"
|
||||
>
|
||||
@@ -1379,7 +1373,6 @@ async function handleSubmitReview(verdict: 'safe' | 'unsafe') {
|
||||
"
|
||||
>
|
||||
<button
|
||||
class="!border-[1px]"
|
||||
:disabled="updatingDetails.has(flag.detail.id)"
|
||||
@click="updateDetailStatus(flag.detail.id, 'unsafe')"
|
||||
>
|
||||
|
||||
@@ -40,11 +40,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled v-if="content" type="outlined">
|
||||
<button
|
||||
class="!border-[1px]"
|
||||
:disabled="!hasPermission"
|
||||
@click="handleSwitchCompatibility"
|
||||
>
|
||||
<button :disabled="!hasPermission" @click="handleSwitchCompatibility">
|
||||
<ArrowLeftRightIcon />
|
||||
Switch type
|
||||
</button>
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled color="medal-promo" type="outlined" size="large" class="z-10 my-auto mt-2">
|
||||
<ButtonStyled color="medal-promo" type="outlined" size="large">
|
||||
<nuxt-link
|
||||
to="https://medal.tv/modrinth"
|
||||
class="z-10 flex w-full items-center justify-center gap-1 md:mt-0 md:w-auto"
|
||||
class="z-10 my-auto mt-2 flex w-full items-center justify-center gap-1 md:mt-0 md:w-auto"
|
||||
>{{ formatMessage(messages.learnMoreButton) }} <ExternalIcon
|
||||
/></nuxt-link>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -23,14 +23,20 @@
|
||||
I confirm that I have properly addressed the moderators' comments.
|
||||
</Checkbox>
|
||||
<div class="input-group push-right">
|
||||
<button
|
||||
class="iconified-button moderation-button"
|
||||
:disabled="!submissionConfirmation"
|
||||
@click="resubmit()"
|
||||
>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Resubmit for review
|
||||
</button>
|
||||
<ButtonStyled color="orange">
|
||||
<button
|
||||
:disabled="!submissionConfirmation || isLoading"
|
||||
@click="runBlockingAction('resubmit-modal', resubmit)"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'resubmit-modal'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ScaleIcon v-else aria-hidden="true" />
|
||||
Resubmit for review
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -55,14 +61,20 @@
|
||||
I acknowledge that the moderators do not actively monitor the thread.
|
||||
</Checkbox>
|
||||
<div class="input-group push-right">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!replyConfirmation"
|
||||
@click="sendReplyFromModal()"
|
||||
>
|
||||
<ReplyIcon aria-hidden="true" />
|
||||
Reply to thread
|
||||
</button>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:disabled="!replyConfirmation || isLoading"
|
||||
@click="runBlockingAction('reply-modal', () => sendReplyFromModal())"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reply-modal'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ReplyIcon v-else aria-hidden="true" />
|
||||
Reply to thread
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -85,10 +97,13 @@
|
||||
</div>
|
||||
<template v-if="report && report.closed">
|
||||
<p>This thread is closed and new messages cannot be sent to it.</p>
|
||||
<button v-if="isStaff(auth.user)" class="iconified-button" @click="reopenReport()">
|
||||
<CheckCircleIcon aria-hidden="true" />
|
||||
Reopen thread
|
||||
</button>
|
||||
<ButtonStyled v-if="isStaff(auth.user)">
|
||||
<button :disabled="isLoading" @click="runBlockingAction('reopen', () => reopenReport())">
|
||||
<SpinnerIcon v-if="loadingAction === 'reopen'" class="animate-spin" aria-hidden="true" />
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
Reopen thread
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else-if="!report || !report.closed">
|
||||
<div class="markdown-editor-spacing">
|
||||
@@ -99,196 +114,241 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button
|
||||
v-if="sortedMessages.length > 0"
|
||||
class="btn btn-primary"
|
||||
:disabled="!replyBody"
|
||||
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
|
||||
>
|
||||
<ReplyIcon aria-hidden="true" />
|
||||
Reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-primary"
|
||||
:disabled="!replyBody"
|
||||
@click="isApproved(project) && !isStaff(auth.user) ? openReplyModal() : sendReply()"
|
||||
>
|
||||
<SendIcon aria-hidden="true" />
|
||||
Send
|
||||
</button>
|
||||
<button
|
||||
v-if="isStaff(auth.user)"
|
||||
class="btn"
|
||||
:disabled="!replyBody"
|
||||
@click="sendReply(null, true)"
|
||||
>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Add private note
|
||||
</button>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
v-if="sortedMessages.length > 0"
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="
|
||||
isApproved(project) && !isStaff(auth.user)
|
||||
? openReplyModal()
|
||||
: runBlockingAction('reply', () => sendReply())
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="loadingAction === 'reply'" class="animate-spin" aria-hidden="true" />
|
||||
<ReplyIcon v-else aria-hidden="true" />
|
||||
Reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="
|
||||
isApproved(project) && !isStaff(auth.user)
|
||||
? openReplyModal()
|
||||
: runBlockingAction('send', () => sendReply())
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="loadingAction === 'send'" class="animate-spin" aria-hidden="true" />
|
||||
<SendIcon v-else aria-hidden="true" />
|
||||
Send
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="isStaff(auth.user)">
|
||||
<button
|
||||
:disabled="!replyBody || isLoading"
|
||||
@click="runBlockingAction('private-note', () => sendReply(null, true))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'private-note'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ScaleIcon v-else aria-hidden="true" />
|
||||
Add private note
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<template v-if="currentMember && !isStaff(auth.user)">
|
||||
<template v-if="isRejected(project)">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
class="iconified-button moderation-button"
|
||||
@click="openResubmitModal(true)"
|
||||
>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Resubmit for review with reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="iconified-button moderation-button"
|
||||
@click="openResubmitModal(false)"
|
||||
>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Resubmit for review
|
||||
</button>
|
||||
<ButtonStyled color="orange">
|
||||
<button v-if="replyBody" :disabled="isLoading" @click="openResubmitModal(true)">
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Resubmit for review with reply
|
||||
</button>
|
||||
<button v-else :disabled="isLoading" @click="openResubmitModal(false)">
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Resubmit for review
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
<div class="spacer"></div>
|
||||
<div class="input-group extra-options">
|
||||
<template v-if="report">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
class="iconified-button danger-button"
|
||||
@click="closeReport(true)"
|
||||
>
|
||||
<CheckCircleIcon aria-hidden="true" />
|
||||
Close with reply
|
||||
</button>
|
||||
<button v-else class="iconified-button danger-button" @click="closeReport()">
|
||||
<CheckCircleIcon aria-hidden="true" />
|
||||
Close thread
|
||||
</button>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
:disabled="isLoading"
|
||||
@click="runBlockingAction('close-with-reply', () => closeReport(true))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'close-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
Close with reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
:disabled="isLoading"
|
||||
@click="runBlockingAction('close', () => closeReport())"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'close'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckCircleIcon v-else aria-hidden="true" />
|
||||
Close thread
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="project">
|
||||
<template v-if="isStaff(auth.user)">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
class="btn btn-green"
|
||||
:disabled="isApproved(project)"
|
||||
@click="sendReply(requestedStatus)"
|
||||
>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
Approve with reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-green"
|
||||
:disabled="isApproved(project)"
|
||||
@click="setStatus(requestedStatus)"
|
||||
>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
Approve
|
||||
</button>
|
||||
<ButtonStyled v-if="replyBody" color="green">
|
||||
<button
|
||||
:disabled="isApproved(project) || isLoading"
|
||||
@click="runBlockingAction('approve-with-reply', () => sendReply(requestedStatus))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'approve-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon v-else aria-hidden="true" />
|
||||
Approve with reply
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="green">
|
||||
<button
|
||||
:disabled="isApproved(project) || isLoading"
|
||||
@click="runBlockingAction('approve', () => setStatus(requestedStatus))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'approve'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CheckIcon v-else aria-hidden="true" />
|
||||
Approve
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<div class="joined-buttons">
|
||||
<button
|
||||
v-if="replyBody"
|
||||
class="btn btn-danger"
|
||||
:disabled="project.status === 'rejected'"
|
||||
@click="sendReply('rejected')"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
Reject with reply
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-danger"
|
||||
:disabled="project.status === 'rejected'"
|
||||
@click="setStatus('rejected')"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
Reject
|
||||
</button>
|
||||
<OverflowMenu
|
||||
class="btn btn-danger btn-dropdown-animation icon-only"
|
||||
:options="
|
||||
replyBody
|
||||
? [
|
||||
{
|
||||
id: 'withhold-reply',
|
||||
color: 'danger',
|
||||
action: () => {
|
||||
sendReply('withheld')
|
||||
<ButtonStyled v-if="replyBody" color="red">
|
||||
<button
|
||||
:disabled="project.status === 'rejected' || isLoading"
|
||||
@click="runBlockingAction('reject-with-reply', () => sendReply('rejected'))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reject-with-reply'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
Reject with reply
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="red">
|
||||
<button
|
||||
:disabled="project.status === 'rejected' || isLoading"
|
||||
@click="runBlockingAction('reject', () => setStatus('rejected'))"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="loadingAction === 'reject'"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
Reject
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<OverflowMenu
|
||||
class="btn-dropdown-animation"
|
||||
:disabled="isLoading"
|
||||
:options="
|
||||
replyBody
|
||||
? [
|
||||
{
|
||||
id: 'withhold-reply',
|
||||
color: 'danger',
|
||||
action: () =>
|
||||
runBlockingAction('withhold-reply', () => sendReply('withheld')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld' || isLoading,
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld',
|
||||
},
|
||||
{
|
||||
id: 'set-to-draft-reply',
|
||||
action: () => {
|
||||
sendReply('draft')
|
||||
{
|
||||
id: 'set-to-draft-reply',
|
||||
action: () =>
|
||||
runBlockingAction('set-to-draft-reply', () => sendReply('draft')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft' || isLoading,
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft',
|
||||
},
|
||||
{
|
||||
id: 'send-to-review-reply',
|
||||
action: () => {
|
||||
sendReply('processing', true)
|
||||
{
|
||||
id: 'send-to-review-reply',
|
||||
action: () =>
|
||||
runBlockingAction('send-to-review-reply', () =>
|
||||
sendReply('processing', true),
|
||||
),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing' || isLoading,
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'withhold',
|
||||
color: 'danger',
|
||||
action: () => {
|
||||
setStatus('withheld')
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'withhold',
|
||||
color: 'danger',
|
||||
action: () =>
|
||||
runBlockingAction('withhold', () => setStatus('withheld')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld' || isLoading,
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'withheld',
|
||||
},
|
||||
{
|
||||
id: 'set-to-draft',
|
||||
action: () => {
|
||||
setStatus('draft')
|
||||
{
|
||||
id: 'set-to-draft',
|
||||
action: () =>
|
||||
runBlockingAction('set-to-draft', () => setStatus('draft')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft' || isLoading,
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'draft',
|
||||
},
|
||||
{
|
||||
id: 'send-to-review',
|
||||
action: () => {
|
||||
setStatus('processing')
|
||||
{
|
||||
id: 'send-to-review',
|
||||
action: () =>
|
||||
runBlockingAction('send-to-review', () => setStatus('processing')),
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing' || isLoading,
|
||||
},
|
||||
hoverFilled: true,
|
||||
disabled: project.status === 'processing',
|
||||
},
|
||||
]
|
||||
"
|
||||
>
|
||||
<DropdownIcon style="rotate: 180deg" aria-hidden="true" />
|
||||
<template #withhold-reply>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
Withhold with reply
|
||||
</template>
|
||||
<template #withhold>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
Withhold
|
||||
</template>
|
||||
<template #set-to-draft-reply>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
Set to draft with reply
|
||||
</template>
|
||||
<template #set-to-draft>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
Set to draft
|
||||
</template>
|
||||
<template #send-to-review-reply>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Send to review with reply
|
||||
</template>
|
||||
<template #send-to-review>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Send to review
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
]
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="isDropdownLoading" class="animate-spin" aria-hidden="true" />
|
||||
<DropdownIcon v-else aria-hidden="true" />
|
||||
<template #withhold-reply>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
Withhold with reply
|
||||
</template>
|
||||
<template #withhold>
|
||||
<EyeOffIcon aria-hidden="true" />
|
||||
Withhold
|
||||
</template>
|
||||
<template #set-to-draft-reply>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
Set to draft with reply
|
||||
</template>
|
||||
<template #set-to-draft>
|
||||
<FileTextIcon aria-hidden="true" />
|
||||
Set to draft
|
||||
</template>
|
||||
<template #send-to-review-reply>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Send to review with reply
|
||||
</template>
|
||||
<template #send-to-review>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Send to review
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -308,9 +368,11 @@ import {
|
||||
ReplyIcon,
|
||||
ScaleIcon,
|
||||
SendIcon,
|
||||
SpinnerIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
CopyCode,
|
||||
injectNotificationManager,
|
||||
@@ -385,6 +447,30 @@ const sortedMessages = computed(() => {
|
||||
const modalSubmit = ref(null)
|
||||
const modalReply = ref(null)
|
||||
|
||||
const loadingAction = ref(null)
|
||||
const isLoading = computed(() => loadingAction.value !== null)
|
||||
const dropdownActionIds = [
|
||||
'withhold',
|
||||
'withhold-reply',
|
||||
'set-to-draft',
|
||||
'set-to-draft-reply',
|
||||
'send-to-review',
|
||||
'send-to-review-reply',
|
||||
]
|
||||
const isDropdownLoading = computed(() => dropdownActionIds.includes(loadingAction.value))
|
||||
|
||||
async function runBlockingAction(actionId, action) {
|
||||
if (loadingAction.value !== null) {
|
||||
return
|
||||
}
|
||||
loadingAction.value = actionId
|
||||
try {
|
||||
await action()
|
||||
} finally {
|
||||
loadingAction.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function updateThreadLocal() {
|
||||
let threadId = null
|
||||
if (props.project) {
|
||||
@@ -412,8 +498,8 @@ async function onUploadImage(file) {
|
||||
}
|
||||
|
||||
async function sendReplyFromModal(status = null, privateMessage = false) {
|
||||
modalReply.value.hide()
|
||||
await sendReply(status, privateMessage)
|
||||
modalReply.value.hide()
|
||||
}
|
||||
|
||||
async function sendReply(status = null, privateMessage = false) {
|
||||
|
||||
@@ -104,20 +104,22 @@
|
||||
</span>
|
||||
</span>
|
||||
<div v-if="isStaff(auth.user) && message.author_id === auth.user.id" class="message__actions">
|
||||
<OverflowMenu
|
||||
class="btn btn-transparent icon-only"
|
||||
:options="[
|
||||
{
|
||||
id: 'delete',
|
||||
action: () => deleteMessage(),
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
</OverflowMenu>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
class="btn-dropdown-animation"
|
||||
:options="[
|
||||
{
|
||||
id: 'delete',
|
||||
action: () => deleteMessage(),
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
<template #delete> <TrashIcon /> Delete </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -135,6 +137,7 @@ import {
|
||||
AutoLink,
|
||||
Avatar,
|
||||
Badge,
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user