mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
106
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae3e61993 | ||
|
|
2562b01483 | ||
|
|
dea690c147 | ||
|
|
eb232cf627 | ||
|
|
2b2ebbaedc | ||
|
|
4a97836998 | ||
|
|
9669d9ef5c | ||
|
|
e7b86e9418 | ||
|
|
b5a5d748ee | ||
|
|
9859e8310e | ||
|
|
501757b9d9 | ||
|
|
e1774217c6 | ||
|
|
bc1c7c80c6 | ||
|
|
23933c1f49 | ||
|
|
da52062fab | ||
|
|
bca1e511ec | ||
|
|
bdc2a60f9b | ||
|
|
96d65902d7 | ||
|
|
6e0fb0d192 | ||
|
|
aad7386023 | ||
|
|
f7ed9e74e0 | ||
|
|
1f1ecc728a | ||
|
|
3ad76ba505 | ||
|
|
78ca6364d7 | ||
|
|
31cadd1f23 | ||
|
|
65a3ac4b34 | ||
|
|
295db3fb7a | ||
|
|
7abe6f8c73 | ||
|
|
c5ce5bc9b3 | ||
|
|
572b8027ff | ||
|
|
0a53211389 | ||
|
|
22b2b7746e | ||
|
|
4989bffa9a | ||
|
|
39b3e20d0a | ||
|
|
3892b62451 | ||
|
|
5ffffec2e5 | ||
|
|
a0724cc5fb | ||
|
|
a0320f425e | ||
|
|
06262e7bb1 | ||
|
|
9ccaab5fad | ||
|
|
4cf6135505 | ||
|
|
2a6db458c4 | ||
|
|
f8aa16f86e | ||
|
|
e1c33ad2da | ||
|
|
39745086fa | ||
|
|
a8dcfa8a5d | ||
|
|
0f6a74b87f | ||
|
|
d5b763c8e3 | ||
|
|
7a98f45c3d | ||
|
|
0ea47ad7ef | ||
|
|
cc804625ec | ||
|
|
20235b7d32 | ||
|
|
920dc8dcbb | ||
|
|
30f64c0020 | ||
|
|
cfafa25ed4 | ||
|
|
7495b99ddd | ||
|
|
800271a606 | ||
|
|
36791a33d8 | ||
|
|
20ec70b917 | ||
|
|
ab6edbee48 | ||
|
|
b17523e7fd | ||
|
|
f89c116bf8 | ||
|
|
cbb31f31c0 | ||
|
|
fe69e04785 | ||
|
|
82348fe40a | ||
|
|
5d34e7902a | ||
|
|
3c8bb0923d | ||
|
|
7c4190fb9c | ||
|
|
ebc7cac8b0 | ||
|
|
224c28f29d | ||
|
|
e192f6a0af | ||
|
|
c938ca3ceb | ||
|
|
20adb0f16a | ||
|
|
354940a8c6 | ||
|
|
4c2e64e31f | ||
|
|
372e2bf47b | ||
|
|
36d786f504 | ||
|
|
1a1e1ea448 | ||
|
|
24240d514a | ||
|
|
158de019a6 | ||
|
|
54cc1898fa | ||
|
|
b96a1cfa58 | ||
|
|
59a794ebb1 | ||
|
|
b4380aee87 | ||
|
|
2cf71a53f2 | ||
|
|
44be49416b | ||
|
|
7f07e66e4f | ||
|
|
907db9e64a | ||
|
|
944f99a14e | ||
|
|
a79736dba9 | ||
|
|
6f0181d52f | ||
|
|
70c544b048 | ||
|
|
5a80df3cd4 | ||
|
|
1e85ec8649 | ||
|
|
1dd613e459 | ||
|
|
d632b7bdbc | ||
|
|
eb8141134a | ||
|
|
cc0d3bf42c | ||
|
|
5e155d69b6 | ||
|
|
707722ded6 | ||
|
|
dfc2ed3783 | ||
|
|
fbe70c6938 | ||
|
|
cd56b98975 | ||
|
|
b06b14e0fb | ||
|
|
a60c0dc395 | ||
|
|
72fa9dd131 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||
[target.'cfg(windows)']
|
||||
[target."cfg(windows)"]
|
||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
BUMP_TYPE: minor
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Setup Node
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
registry-url: https://registry.npmjs.org
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Restore pnpm cache
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # 0.12.1
|
||||
uses: styfle/cancel-workflow-action@d07a454dad7609a92316b57b23c9ccfd4f59af66 # 0.13.1
|
||||
with:
|
||||
workflow_id: all
|
||||
access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Post or update changelog comment
|
||||
uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -12,14 +12,14 @@ jobs:
|
||||
typos:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: crate-ci/typos@6ac2ebd1b93eade61faf7e12688ad87a073fea59 # v1.46.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0
|
||||
|
||||
# see <https://github.com/influxdata/datafusion-udf-wasm/pull/275>
|
||||
tombi:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: taiki-e/install-action@fd2f5e3d644b484055ebf4268f474c565f148f25 # v2.81.9
|
||||
with:
|
||||
tool: tombi
|
||||
|
||||
@@ -13,8 +13,8 @@ jobs:
|
||||
shear:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
- uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
|
||||
- uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Check if workflow runs on an internal branch
|
||||
id: check-internal
|
||||
@@ -72,19 +72,15 @@ jobs:
|
||||
fi
|
||||
|
||||
docker:
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
env:
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'namespace-profile-medium-amd64' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
|
||||
with:
|
||||
rustflags: ''
|
||||
cache: false
|
||||
@@ -92,30 +88,6 @@ jobs:
|
||||
- name: Setup mold
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Ensure sccache directory exists
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-daedalus-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Build daedalus_client
|
||||
run: cargo build --release --package daedalus_client
|
||||
|
||||
@@ -125,11 +97,11 @@ jobs:
|
||||
cp target/release/daedalus_client apps/daedalus_client/docker-stage/daedalus_client
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Generate Docker image metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
with:
|
||||
@@ -144,14 +116,14 @@ jobs:
|
||||
org.opencontainers.image.licenses=MIT
|
||||
|
||||
- name: Login to GitHub Packages
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: ./apps/daedalus_client/docker-stage
|
||||
file: ./apps/daedalus_client/Dockerfile
|
||||
|
||||
@@ -12,10 +12,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -39,14 +39,14 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: namespace-profile-modrinth-frontend
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -90,24 +90,23 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore pnpm cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
- name: Set up caches
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
cache: |
|
||||
rust
|
||||
pnpm
|
||||
apt
|
||||
|
||||
- name: Set up Turborepo caching
|
||||
uses: namespace-actions/setup-turbocache@f96bd6befa13ae74b2858a5be49573640d4636f5 # v0.0.2
|
||||
|
||||
- name: Inject build variables
|
||||
working-directory: ./apps/frontend
|
||||
@@ -136,7 +135,7 @@ jobs:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Create Sentry release and upload sourcemaps
|
||||
uses: getsentry/action-release@5657c9e888b4e2cc85f4d29143ea4131fde4a73a # v3.6.0
|
||||
uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3.7.0
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: modrinth
|
||||
@@ -146,50 +145,53 @@ jobs:
|
||||
sourcemaps: ./apps/frontend/.output/server
|
||||
url_prefix: '~/'
|
||||
|
||||
- name: Deploy Cloudflare Worker
|
||||
id: wrangler
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
- name: Install Wrangler CLI
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
environment: ${{ steps.meta.outputs.env != 'production' && steps.meta.outputs.env || '' }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.cmd }}
|
||||
wranglerVersion: '4.112.0'
|
||||
command: --version
|
||||
|
||||
- name: Deploy staging Cloudflare Worker preview
|
||||
if: ${{ inputs.preview }}
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.staging_cmd }}
|
||||
- parallel:
|
||||
- name: Deploy Cloudflare Worker
|
||||
id: wrangler
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
environment: ${{ steps.meta.outputs.env != 'production' && steps.meta.outputs.env || '' }}
|
||||
workingDirectory: ./apps/frontend
|
||||
command: ${{ steps.meta.outputs.cmd }}
|
||||
|
||||
- name: Deploy persistent Cloudflare Worker preview
|
||||
if: ${{ inputs.preview && inputs.branch_alias != '' }}
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: versions upload --preview-alias ${{ inputs.branch_alias }} --var PREVIEW:true --var CF_PAGES_URL:${{ steps.meta.outputs.branch_alias_url }}
|
||||
- name: Deploy staging Cloudflare Worker preview
|
||||
if: ${{ inputs.preview }}
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
command: ${{ steps.meta.outputs.staging_cmd }}
|
||||
|
||||
- name: Deploy persistent staging Cloudflare Worker preview
|
||||
if: ${{ inputs.preview && inputs.branch_alias != '' }}
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: ${{ steps.meta.outputs.staging_branch_alias_cmd }}
|
||||
- name: Deploy persistent Cloudflare Worker preview
|
||||
if: ${{ inputs.preview && inputs.branch_alias != '' }}
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
command: versions upload --preview-alias ${{ inputs.branch_alias }} --var PREVIEW:true --var CF_PAGES_URL:${{ steps.meta.outputs.branch_alias_url }}
|
||||
|
||||
- name: Deploy persistent staging Cloudflare Worker preview
|
||||
if: ${{ inputs.preview && inputs.branch_alias != '' }}
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./apps/frontend
|
||||
command: ${{ steps.meta.outputs.staging_branch_alias_cmd }}
|
||||
|
||||
- name: Purge cache
|
||||
if: github.ref == 'refs/heads/prod'
|
||||
@@ -206,16 +208,17 @@ jobs:
|
||||
echo "${{ steps.meta.outputs.url }}" > deployment-url-production.txt
|
||||
echo "${{ steps.meta.outputs.staging_url }}" > deployment-url-staging.txt
|
||||
|
||||
- name: Upload production deployment URL
|
||||
if: ${{ inputs.preview }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: deployment-url-production
|
||||
path: deployment-url-production.txt
|
||||
- parallel:
|
||||
- name: Upload production deployment URL
|
||||
if: ${{ inputs.preview }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: deployment-url-production
|
||||
path: deployment-url-production.txt
|
||||
|
||||
- name: Upload staging deployment URL
|
||||
if: ${{ inputs.preview }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: deployment-url-staging
|
||||
path: deployment-url-staging.txt
|
||||
- name: Upload staging deployment URL
|
||||
if: ${{ inputs.preview }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: deployment-url-staging
|
||||
path: deployment-url-staging.txt
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
deploy-storybook:
|
||||
if: github.repository_owner == 'modrinth' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
needs: metadata
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
runs-on: namespace-profile-modrinth-frontend
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-storybook
|
||||
cancel-in-progress: true
|
||||
@@ -48,27 +48,21 @@ jobs:
|
||||
deployments: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore pnpm cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
- name: Set up caches
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
cache: |
|
||||
pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./packages/ui
|
||||
@@ -86,27 +80,34 @@ jobs:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: echo "sha_short=${HEAD_SHA::8}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Deploy Storybook preview
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
- name: Install Wrangler CLI
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./packages/ui
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }}
|
||||
wranglerVersion: '4.112.0'
|
||||
command: --version
|
||||
|
||||
- name: Deploy persistent Storybook preview
|
||||
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3.15.0
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./packages/ui
|
||||
packageManager: pnpm
|
||||
wranglerVersion: '4.54.0'
|
||||
command: versions upload --preview-alias ${{ needs.metadata.outputs.branch-alias }}
|
||||
- parallel:
|
||||
- name: Deploy Storybook preview
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./packages/ui
|
||||
command: versions upload --preview-alias git-${{ steps.meta.outputs.sha_short }}
|
||||
|
||||
- name: Deploy persistent Storybook preview
|
||||
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: ./packages/ui
|
||||
command: versions upload --preview-alias ${{ needs.metadata.outputs.branch-alias }}
|
||||
|
||||
comment:
|
||||
if: |
|
||||
|
||||
@@ -51,13 +51,13 @@ jobs:
|
||||
CROWDIN_GH_TOKEN_DEFINED: ${{ secrets.CROWDIN_GH_TOKEN != '' }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ secrets.CROWDIN_GH_TOKEN }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download translations from Crowdin
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
|
||||
@@ -56,13 +56,13 @@ jobs:
|
||||
CROWDIN_PERSONAL_TOKEN_DEFINED: ${{ secrets.CROWDIN_PERSONAL_TOKEN != '' }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
echo "safe_branch_name=$SAFE_BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload translations to Crowdin
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Check if workflow runs on an internal branch
|
||||
id: check-internal
|
||||
@@ -70,22 +70,21 @@ jobs:
|
||||
fi
|
||||
|
||||
docker:
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'namespace-profile-modrinth-labrinth' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
env:
|
||||
SQLX_OFFLINE: 'true'
|
||||
GIT_HASH: ${{ github.sha }}
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '20G' || '' }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
RUST_MIN_STACK: '16777216'
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
|
||||
with:
|
||||
rustflags: ''
|
||||
cache: false
|
||||
@@ -93,36 +92,23 @@ jobs:
|
||||
- name: Setup mold
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Set up caches
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
cache: |
|
||||
rust
|
||||
apt
|
||||
|
||||
- name: Configure sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
run: nsc cache sccache setup --cache_name default >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install build dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake libcurl4-openssl-dev
|
||||
#uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
#with:
|
||||
# packages: cmake libcurl4-openssl-dev
|
||||
# version: v2 # cache key
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Ensure sccache directory exists
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
run: sudo mkdir -p /mnt/sccache && sudo chown $(id -u):$(id -g) /mnt/sccache
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-labrinth-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Build labrinth
|
||||
run: cargo build --profile release-labrinth --package labrinth
|
||||
@@ -135,11 +121,11 @@ jobs:
|
||||
cp -r apps/labrinth/assets apps/labrinth/docker-stage/assets
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Generate Docker image metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
env:
|
||||
# GitHub Packages requires annotations metadata in at least the index descriptor to show them
|
||||
# up properly in its UI it seems, but it's not clear about it, because the docs refer to the
|
||||
@@ -158,14 +144,14 @@ jobs:
|
||||
org.opencontainers.image.licenses=AGPL-3.0-only
|
||||
|
||||
- name: Login to GitHub Packages
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: ./apps/labrinth/docker-stage
|
||||
file: ./apps/labrinth/Dockerfile
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Prepare pnpm cache
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/prepare-pnpm-cache.yml
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/prod' }}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm recursive install --frozen-lockfile
|
||||
@@ -44,61 +44,62 @@ jobs:
|
||||
name: Build
|
||||
env:
|
||||
VITE_STRIPE_PUBLISHABLE_KEY: pk_live_51JbFxJJygY5LJFfKLVVldb10HlLt24p421OWRsTOWc5sXYFOnFUXWieSc6HD3PHo25ktx8db1WcHr36XGFvZFVUz00V9ixrCs5
|
||||
# SCCACHE_DIR: '/mnt/sccache'
|
||||
# SCCACHE_CACHE_SIZE: '10G'
|
||||
# SCCACHE_MULTILEVEL_CHAIN: 'disk,s3'
|
||||
SCCACHE_S3_KEY_PREFIX: '${{ github.repository }}/'
|
||||
SCCACHE_BUCKET: ${{ secrets.SCCACHE_BUCKET }}
|
||||
SCCACHE_REGION: ${{ secrets.SCCACHE_REGION }}
|
||||
SCCACHE_ENDPOINT: ${{ secrets.SCCACHE_ENDPOINT }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_ACCESS_KEY }}
|
||||
RUSTC_WRAPPER: 'sccache'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [
|
||||
blacksmith-6vcpu-macos-26,
|
||||
blacksmith-8vcpu-windows-2025, # At time of writing, Windows 4 vCPU VMs don't seem to actually exist
|
||||
blacksmith-4vcpu-ubuntu-2404,
|
||||
platform:
|
||||
[
|
||||
namespace-profile-medium-amd64-macos,
|
||||
namespace-profile-medium-amd64-windows,
|
||||
namespace-profile-medium-amd64,
|
||||
]
|
||||
include:
|
||||
- platform: blacksmith-6vcpu-macos-26
|
||||
- platform: namespace-profile-medium-amd64-macos
|
||||
artifact-target-name: universal-apple-darwin
|
||||
- platform: blacksmith-8vcpu-windows-2025
|
||||
- platform: namespace-profile-medium-amd64-windows
|
||||
artifact-target-name: x86_64-pc-windows-msvc
|
||||
- platform: blacksmith-4vcpu-ubuntu-2404
|
||||
- platform: namespace-profile-medium-amd64
|
||||
artifact-target-name: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
|
||||
with:
|
||||
rustflags: ''
|
||||
target: ${{ contains(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
target: ${{ contains(matrix.artifact-target-name, 'darwin') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Setup mold
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
if: contains(matrix.artifact-target-name, 'linux')
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Setup sccache
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
- name: Set up caches
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: 'pnpm'
|
||||
cache: |
|
||||
rust
|
||||
pnpm
|
||||
|
||||
- name: Configure sccache
|
||||
run: nsc cache sccache setup --cache_name default >> "$GITHUB_ENV"
|
||||
|
||||
- name: Generate tauri-dev.conf.json
|
||||
shell: bash
|
||||
@@ -116,11 +117,16 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
if: contains(matrix.artifact-target-name, 'linux')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
version: v1 # cache key
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Dasel
|
||||
uses: jaxxstorm/action-install-gh-release@25e24d2d23ae098373794ef1d6faecb48ee52da8 # v3.0.0
|
||||
@@ -128,7 +134,7 @@ jobs:
|
||||
repo: TomWright/dasel
|
||||
tag: v2.8.1
|
||||
extension-matching: disable
|
||||
rename-to: ${{ contains(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
rename-to: ${{ contains(matrix.artifact-target-name, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
chmod: 0755
|
||||
|
||||
- name: Set application version and environment
|
||||
@@ -147,14 +153,11 @@ jobs:
|
||||
|
||||
cp "packages/app-lib/.env.${BUILD_ENVIRONMENT}" packages/app-lib/.env
|
||||
|
||||
- name: Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set up Windows code signing
|
||||
if: contains(matrix.platform, 'windows')
|
||||
if: contains(matrix.artifact-target-name, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then
|
||||
@@ -165,7 +168,7 @@ jobs:
|
||||
|
||||
- name: Build macOS app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-dev.conf.json' }}
|
||||
if: contains(matrix.platform, 'macos')
|
||||
if: contains(matrix.artifact-target-name, 'darwin')
|
||||
env:
|
||||
TAURI_BUNDLER_DMG_IGNORE_CI: 'true'
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -180,7 +183,7 @@ jobs:
|
||||
|
||||
- name: Build Linux app
|
||||
run: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json' }}
|
||||
if: contains(matrix.platform, 'ubuntu')
|
||||
if: contains(matrix.artifact-target-name, 'linux')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
@@ -192,7 +195,7 @@ jobs:
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_17_X64"
|
||||
${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || inputs.app-version-override != '') && 'pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles "nsis,updater"' || 'pnpm --filter=@modrinth/app run tauri build --config tauri-dev.conf.json --verbose --bundles "nsis,updater"' }}
|
||||
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
||||
if: contains(matrix.platform, 'windows')
|
||||
if: contains(matrix.artifact-target-name, 'windows')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_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-4vcpu-ubuntu-2404
|
||||
runs-on: namespace-profile-medium-amd64
|
||||
env:
|
||||
VERSION_TAG: ${{ github.event.workflow_run.head_branch }}
|
||||
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Verify ref is a tag
|
||||
env:
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Check if workflow runs on an internal branch
|
||||
id: check-internal
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Lint and Test
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
runs-on: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'namespace-profile-modrinth-turbo' || 'ubuntu-latest' }}
|
||||
needs: [skip-if-clean]
|
||||
if: ${{ needs.skip-if-clean.outputs.skip != 'true' }}
|
||||
|
||||
@@ -76,54 +76,47 @@ jobs:
|
||||
# since we don't want warnings to become errors
|
||||
# while developing)
|
||||
RUSTFLAGS: -Dwarnings
|
||||
# sccache config (only populated for internal branches; secrets aren't
|
||||
# available to forked PRs, and blacksmith stickydisk requires a
|
||||
# blacksmith runner)
|
||||
SCCACHE_DIR: ${{ needs.skip-if-clean.outputs.internal == 'true' && '/mnt/sccache' || '' }}
|
||||
SCCACHE_CACHE_SIZE: ${{ needs.skip-if-clean.outputs.internal == 'true' && '10G' || '' }}
|
||||
RUSTC_WRAPPER: ${{ needs.skip-if-clean.outputs.internal == 'true' && 'sccache' || '' }}
|
||||
CARGO_INCREMENTAL: 0
|
||||
# Redis config for tests
|
||||
REDIS_TOPOLOGY: cluster
|
||||
REDIS_CONNECTION_TYPE: multiplexed
|
||||
REDIS_URL: redis://127.0.0.1:7000,redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003,redis://127.0.0.1:7004,redis://127.0.0.1:7005
|
||||
# Avoid stack overflows in tests
|
||||
RUST_MIN_STACK: 16777216
|
||||
RUST_MIN_STACK: 134217728
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install build dependencies
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
|
||||
with:
|
||||
packages: cmake libcurl4-openssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
version: v2 # cache key
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore pnpm cache
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
- name: Set up caches
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.store-path }}
|
||||
key: pnpm-cache-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
pnpm-cache-
|
||||
cache: |
|
||||
rust
|
||||
pnpm
|
||||
apt
|
||||
|
||||
- name: Set up Turborepo caching
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: namespace-actions/setup-turbocache@f96bd6befa13ae74b2858a5be49573640d4636f5 # v0.0.2
|
||||
|
||||
- name: Install apt dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y cmake libcurl4-openssl-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@2b1f5e9b395427c92ee4e3331786ca3c37afe2d7 # v1.16.0
|
||||
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
|
||||
with:
|
||||
rustflags: ''
|
||||
components: clippy, rustfmt
|
||||
@@ -132,28 +125,8 @@ jobs:
|
||||
- name: Setup mold
|
||||
uses: rui314/setup-mold@9c9c13bf4c3f1adef0cc596abc155580bcb04444 # v1 / Mold 2.41.0
|
||||
|
||||
- name: Cache Cargo registry and index
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Mount sccache disk cache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: useblacksmith/stickydisk@13af8883542ca949a717e70fef89d15edbb29d88 # v1.2.0
|
||||
with:
|
||||
key: ${{ github.repository }}-turbo-sccache
|
||||
path: /mnt/sccache
|
||||
|
||||
- name: Setup sccache
|
||||
if: needs.skip-if-clean.outputs.internal == 'true'
|
||||
uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10
|
||||
|
||||
- name: Setup binstall
|
||||
uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
|
||||
uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0
|
||||
|
||||
- name: Setup nextest
|
||||
run: cargo binstall --no-confirm --secure --locked cargo-nextest@0.9.133
|
||||
@@ -161,16 +134,13 @@ jobs:
|
||||
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||
# back to a cached cargo install
|
||||
- name: Setup cargo-sqlx
|
||||
uses: taiki-e/cache-cargo-install-action@f9eed3e4680f27610dc6d8c67be1b88593f7dade # v3.0.6
|
||||
uses: taiki-e/cache-cargo-install-action@417450f3c33ee20393705369577571770643d4c7 # v3.0.7
|
||||
with:
|
||||
tool: sqlx-cli@0.8.6
|
||||
locked: false
|
||||
no-default-features: true
|
||||
features: rustls,postgres
|
||||
|
||||
- name: Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
|
||||
Generated
+6
-3
@@ -4806,7 +4806,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.5",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
@@ -5465,6 +5465,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"censor",
|
||||
"chrono",
|
||||
"chumsky",
|
||||
"clap 4.5.48",
|
||||
"clickhouse",
|
||||
"color-eyre",
|
||||
@@ -5518,6 +5519,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"sha1 0.10.6",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"spdx",
|
||||
"sqlx",
|
||||
"sqlx-tracing",
|
||||
@@ -8014,7 +8016,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.32",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.5",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -8051,7 +8053,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.6.5",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
@@ -11018,6 +11020,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"hickory-resolver 0.25.2",
|
||||
"httpdate",
|
||||
"image",
|
||||
"indicatif",
|
||||
"itertools 0.14.0",
|
||||
"modrinth-content-management",
|
||||
|
||||
@@ -59,6 +59,7 @@ bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.42"
|
||||
chumsky = "0.9.3"
|
||||
cidre = { version = "0.15.0", default-features = false, features = [
|
||||
"macos_15_0"
|
||||
] }
|
||||
@@ -192,6 +193,7 @@ sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
shlex = "1.3.0"
|
||||
smallvec = "1.15.1"
|
||||
spdx = "0.12.0"
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
sqlx-tracing = { path = "packages/sqlx-tracing" }
|
||||
|
||||
+202
-241
@@ -10,45 +10,45 @@ import {
|
||||
} from '@modrinth/api-client'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChangeSkinIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CompassIcon,
|
||||
ExternalIcon,
|
||||
HomeIcon,
|
||||
LeftArrowIcon,
|
||||
LibraryIcon,
|
||||
LogInIcon,
|
||||
LogOutIcon,
|
||||
NewspaperIcon,
|
||||
NotepadTextIcon,
|
||||
PlusIcon,
|
||||
RefreshCwIcon,
|
||||
RightArrowIcon,
|
||||
ServerStackIcon,
|
||||
SettingsIcon,
|
||||
ShirtIcon,
|
||||
UserIcon,
|
||||
WorldIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
ButtonLink,
|
||||
commonMessages,
|
||||
ContentInstallModal,
|
||||
ContentUpdaterModal,
|
||||
CreationFlowModal,
|
||||
defineMessages,
|
||||
I18nDebugPanel,
|
||||
IconButton,
|
||||
IntlFormatted,
|
||||
LoadingBar,
|
||||
NewsArticleCard,
|
||||
NotificationPanel,
|
||||
OverflowMenu,
|
||||
PopupNotificationPanel,
|
||||
provideModalBehavior,
|
||||
provideModrinthClient,
|
||||
provideNotificationManager,
|
||||
providePageContext,
|
||||
providePopupNotificationManager,
|
||||
TeleportOverflowMenu,
|
||||
TextLogo,
|
||||
useDebugLogger,
|
||||
useFormatBytes,
|
||||
useHostingIntercom,
|
||||
@@ -63,11 +63,9 @@ import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { $fetch } from 'ofetch'
|
||||
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import AppActionBar from '@/components/ui/AppActionBar.vue'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
@@ -88,15 +86,16 @@ import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
import SharedInstanceInviteHandler from '@/components/ui/shared-instances/shared-instance-invite-handler/index.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import SurveyPopup from '@/components/ui/SurveyPopup.vue'
|
||||
import WindowControls from '@/components/ui/WindowControls.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import { config } from '@/config'
|
||||
import {
|
||||
ads_consent_listener,
|
||||
get_ads_consent_required,
|
||||
hide_ads_window,
|
||||
init_ads_window,
|
||||
perform_ads_consent_action,
|
||||
should_show_ads_consent_popup,
|
||||
show_ads_window,
|
||||
} from '@/helpers/ads.js'
|
||||
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
@@ -104,17 +103,13 @@ import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user, get_version } from '@/helpers/cache.js'
|
||||
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
|
||||
import {
|
||||
can_current_user_use_shared_instances,
|
||||
get as getInstance,
|
||||
list,
|
||||
run,
|
||||
} from '@/helpers/instance'
|
||||
import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance'
|
||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
|
||||
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||
import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts'
|
||||
import { parse_modrinth_user_link } from '@/helpers/users'
|
||||
import {
|
||||
areUpdatesEnabled,
|
||||
enqueueUpdateForInstallation,
|
||||
@@ -136,6 +131,7 @@ import {
|
||||
openAppUpdateChangelog,
|
||||
setAppUpdateActions,
|
||||
} from '@/providers/app-update.ts'
|
||||
import { createBreadcrumbManager, provideBreadcrumbManager } from '@/providers/breadcrumbs'
|
||||
import { createContentInstall, provideContentInstall } from '@/providers/content-install'
|
||||
import {
|
||||
provideAppUpdateDownloadProgress,
|
||||
@@ -147,15 +143,30 @@ import { setupAuthProvider } from '@/providers/setup/auth'
|
||||
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
|
||||
import { useError } from '@/store/error.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { appMessages } from '@/utils/app-messages'
|
||||
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
import { AppNotificationManager } from './providers/app-notifications'
|
||||
import { AppPopupNotificationManager } from './providers/app-popup-notifications'
|
||||
import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const breadcrumbManager = createBreadcrumbManager()
|
||||
provideBreadcrumbManager(breadcrumbManager)
|
||||
const canNavigateBack = ref(false)
|
||||
const canNavigateForward = ref(false)
|
||||
|
||||
function updateHistoryNavigationState() {
|
||||
const historyState = window.history.state
|
||||
canNavigateBack.value = historyState?.back != null
|
||||
canNavigateForward.value = historyState?.forward != null
|
||||
}
|
||||
|
||||
updateHistoryNavigationState()
|
||||
|
||||
const APP_LEFT_NAV_WIDTH = '4rem'
|
||||
const APP_SIDEBAR_WIDTH = 300
|
||||
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
||||
@@ -265,7 +276,7 @@ providePageContext({
|
||||
themeStore.getFeatureFlag('server_ram_as_bytes_always_on'),
|
||||
),
|
||||
},
|
||||
openExternalUrl: (url) => openUrl(url),
|
||||
openExternalUrl: (url) => void openUrl(url),
|
||||
})
|
||||
provideModalBehavior({
|
||||
noblur: computed(() => !themeStore.advancedRendering),
|
||||
@@ -288,8 +299,10 @@ const {
|
||||
} = setupProviders(notificationManager, popupNotificationManager)
|
||||
|
||||
const news = ref([])
|
||||
const availableSurvey = ref(false)
|
||||
const displayedServerInviteNotifications = new Set()
|
||||
const serverInvitePopupNotificationIds = new Set()
|
||||
let liveNotificationGeneration = 0
|
||||
let liveNotificationsEnabled = true
|
||||
|
||||
const offline = ref(!navigator.onLine)
|
||||
window.addEventListener('offline', () => {
|
||||
@@ -336,7 +349,7 @@ onMounted(async () => {
|
||||
await useCheckDisableMouseover()
|
||||
try {
|
||||
unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired)
|
||||
handleAdsConsentRequired(await get_ads_consent_required())
|
||||
handleAdsConsentRequired(await should_show_ads_consent_popup())
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
@@ -399,6 +412,54 @@ const messages = defineMessages({
|
||||
id: 'app.ads-consent.accept',
|
||||
defaultMessage: 'Accept all',
|
||||
},
|
||||
home: {
|
||||
id: 'app.nav.home',
|
||||
defaultMessage: 'Home',
|
||||
},
|
||||
library: {
|
||||
id: 'app.nav.library',
|
||||
defaultMessage: 'Library',
|
||||
},
|
||||
modrinthHosting: {
|
||||
id: 'app.nav.modrinth-hosting',
|
||||
defaultMessage: 'Modrinth Hosting',
|
||||
},
|
||||
createNewInstance: {
|
||||
id: 'app.nav.create-new-instance',
|
||||
defaultMessage: 'Create new instance',
|
||||
},
|
||||
modrinthAccount: {
|
||||
id: 'app.nav.modrinth-account',
|
||||
defaultMessage: 'Modrinth account',
|
||||
},
|
||||
signedInAs: {
|
||||
id: 'app.nav.signed-in-as',
|
||||
defaultMessage: 'Signed in as <user>{username}</user>',
|
||||
},
|
||||
signInToModrinthAccount: {
|
||||
id: 'app.nav.sign-in-to-modrinth-account',
|
||||
defaultMessage: 'Sign in to a Modrinth account',
|
||||
},
|
||||
restarting: {
|
||||
id: 'app.restarting',
|
||||
defaultMessage: 'Restarting...',
|
||||
},
|
||||
upgradeToModrinthPlus: {
|
||||
id: 'app.nav.upgrade-to-modrinth-plus',
|
||||
defaultMessage: 'Upgrade to Modrinth+',
|
||||
},
|
||||
news: {
|
||||
id: 'app.news.title',
|
||||
defaultMessage: 'News',
|
||||
},
|
||||
viewAllNews: {
|
||||
id: 'app.news.view-all',
|
||||
defaultMessage: 'View all news',
|
||||
},
|
||||
playingAs: {
|
||||
id: 'app.sidebar.playing-as',
|
||||
defaultMessage: 'Playing as',
|
||||
},
|
||||
})
|
||||
|
||||
function handleAdsConsentRequired(required) {
|
||||
@@ -567,12 +628,6 @@ async function setupApp() {
|
||||
settings.pending_update_toast_for_version = null
|
||||
await setSettings(settings)
|
||||
}
|
||||
|
||||
if (osType === 'windows') {
|
||||
await processPendingSurveys()
|
||||
} else {
|
||||
console.info('Skipping user surveys on non-Windows platforms')
|
||||
}
|
||||
}
|
||||
|
||||
const stateFailed = ref(false)
|
||||
@@ -616,6 +671,7 @@ router.beforeEach(() => {
|
||||
routerToken = loading.begin()
|
||||
})
|
||||
router.afterEach((to, from, failure) => {
|
||||
updateHistoryNavigationState()
|
||||
trackEvent('PageView', {
|
||||
path: to.path,
|
||||
fromPath: from.path,
|
||||
@@ -757,6 +813,8 @@ const sharedInstanceInviteHandler = ref()
|
||||
const updateToPlayModal = ref()
|
||||
|
||||
const modrinthLoginModal = ref()
|
||||
const appSettingsModal = ref()
|
||||
provide(appSettingsModalOpenProfileKey, () => appSettingsModal.value?.showProfile())
|
||||
|
||||
watch(incompatibilityWarningModal, (modal) => {
|
||||
if (modal) {
|
||||
@@ -786,16 +844,19 @@ async function validateSession(sessionToken) {
|
||||
}
|
||||
|
||||
async function fetchCredentials() {
|
||||
const hadSession = !!credentials.value?.session
|
||||
const refreshId = ++credentialsRefreshId
|
||||
credentials.value = undefined
|
||||
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
if (!creds && hadSession) clearLiveNotifications()
|
||||
|
||||
if (creds && creds.user_id) {
|
||||
if (creds.session && !(await validateSession(creds.session))) {
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
|
||||
clearLiveNotifications()
|
||||
await logout().catch(handleError)
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
|
||||
@@ -806,6 +867,7 @@ async function fetchCredentials() {
|
||||
if (refreshId !== credentialsRefreshId) return
|
||||
}
|
||||
credentials.value = creds ?? null
|
||||
liveNotificationsEnabled = !!creds?.session
|
||||
}
|
||||
|
||||
async function signIn(flow = 'sign-in') {
|
||||
@@ -841,6 +903,7 @@ async function logOut() {
|
||||
async function performLogOut() {
|
||||
credentialsRefreshId++
|
||||
credentials.value = undefined
|
||||
clearLiveNotifications()
|
||||
|
||||
await logout().catch(handleError)
|
||||
await fetchCredentials()
|
||||
@@ -958,16 +1021,17 @@ async function declineServerInviteNotification(notification) {
|
||||
|
||||
function openServerInviteInviterProfile(inviterName) {
|
||||
if (!inviterName) return
|
||||
openUrl(`${config.siteUrl}/user/${encodeURIComponent(inviterName)}`)
|
||||
void router.push(`/user/${encodeURIComponent(inviterName)}`)
|
||||
}
|
||||
|
||||
async function handleLiveNotification(notification) {
|
||||
if (!notification?.body || notification.read) return
|
||||
if (!liveNotificationsEnabled || !notification?.body || notification.read) return
|
||||
if (await sharedInstanceInviteHandler.value?.handleNotification(notification)) return
|
||||
|
||||
if (notification.body.type === 'server_invite') {
|
||||
if (displayedServerInviteNotifications.has(notification.id)) return
|
||||
|
||||
const generation = liveNotificationGeneration
|
||||
displayedServerInviteNotifications.add(notification.id)
|
||||
|
||||
const serverName =
|
||||
@@ -975,8 +1039,9 @@ async function handleLiveNotification(notification) {
|
||||
const inviterId = notification.body.invited_by
|
||||
const invitedBy =
|
||||
typeof inviterId === 'string' ? await get_user(inviterId, 'bypass').catch(() => null) : null
|
||||
if (generation !== liveNotificationGeneration) return
|
||||
|
||||
addPopupNotification({
|
||||
const popupNotification = addPopupNotification({
|
||||
title: serverName,
|
||||
autoCloseMs: null,
|
||||
toast: {
|
||||
@@ -989,9 +1054,21 @@ async function handleLiveNotification(notification) {
|
||||
onOpenActor: () => openServerInviteInviterProfile(invitedBy?.username ?? null),
|
||||
},
|
||||
})
|
||||
serverInvitePopupNotificationIds.add(popupNotification.id)
|
||||
}
|
||||
}
|
||||
|
||||
function clearLiveNotifications() {
|
||||
liveNotificationGeneration++
|
||||
liveNotificationsEnabled = false
|
||||
for (const id of serverInvitePopupNotificationIds) {
|
||||
popupNotificationManager.removeNotification(id)
|
||||
}
|
||||
displayedServerInviteNotifications.clear()
|
||||
serverInvitePopupNotificationIds.clear()
|
||||
sharedInstanceInviteHandler.value?.clearNotifications()
|
||||
}
|
||||
|
||||
async function handleCommand(e) {
|
||||
if (!e) return
|
||||
|
||||
@@ -1385,8 +1462,11 @@ function handleClick(e) {
|
||||
!target.href.startsWith('https://tauri.localhost') &&
|
||||
!target.href.startsWith('http://tauri.localhost')
|
||||
) {
|
||||
const userPath = parse_modrinth_user_link(target.href)
|
||||
const parsed = parseModrinthLink(target.href)
|
||||
if (target.target !== '_blank' && parsed) {
|
||||
if (userPath) {
|
||||
void router.push(userPath)
|
||||
} else if (target.target !== '_blank' && parsed) {
|
||||
void openModrinthProjectLinkInApp(parsed)
|
||||
} else {
|
||||
openUrl(target.href)
|
||||
@@ -1413,115 +1493,6 @@ function handleAuxClick(e) {
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupOldSurveyDisplayData() {
|
||||
const threeWeeksAgo = new Date()
|
||||
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21)
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
|
||||
if (key.startsWith('survey-') && key.endsWith('-display')) {
|
||||
const dateValue = new Date(localStorage.getItem(key))
|
||||
if (dateValue < threeWeeksAgo) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openSurvey() {
|
||||
if (!availableSurvey.value) {
|
||||
console.error('No survey to open')
|
||||
return
|
||||
}
|
||||
|
||||
const creds = await getCreds().catch(handleError)
|
||||
const userId = creds?.user_id
|
||||
|
||||
const formId = availableSurvey.value.tally_id
|
||||
|
||||
const popupOptions = {
|
||||
layout: 'modal',
|
||||
width: 700,
|
||||
autoClose: 2000,
|
||||
hideTitle: true,
|
||||
hiddenFields: {
|
||||
user_id: userId,
|
||||
},
|
||||
onOpen: () => console.info('Opened user survey'),
|
||||
onClose: () => {
|
||||
console.info('Closed user survey')
|
||||
show_ads_window()
|
||||
},
|
||||
onSubmit: () => console.info('Active user survey submitted'),
|
||||
}
|
||||
|
||||
try {
|
||||
hide_ads_window()
|
||||
if (window.Tally?.openPopup) {
|
||||
console.info(`Opening Tally popup for user survey (form ID: ${formId})`)
|
||||
dismissSurvey()
|
||||
window.Tally.openPopup(formId, popupOptions)
|
||||
} else {
|
||||
console.warn('Tally script not yet loaded')
|
||||
show_ads_window()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error opening Tally popup:', e)
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
console.info(`Found user survey to show with tally_id: ${formId}`)
|
||||
window.Tally.openPopup(formId, popupOptions)
|
||||
}
|
||||
|
||||
function dismissSurvey() {
|
||||
localStorage.setItem(`survey-${availableSurvey.value.id}-display`, new Date())
|
||||
availableSurvey.value = undefined
|
||||
}
|
||||
|
||||
async function processPendingSurveys() {
|
||||
function isWithinLastTwoWeeks(date) {
|
||||
const twoWeeksAgo = new Date()
|
||||
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14)
|
||||
return date >= twoWeeksAgo
|
||||
}
|
||||
|
||||
cleanupOldSurveyDisplayData()
|
||||
|
||||
const creds = await getCreds().catch(handleError)
|
||||
const userId = creds?.user_id
|
||||
|
||||
const instances = (await list().catch(handleError)) ?? []
|
||||
const isActivePlayer = instances.some(
|
||||
(instance) =>
|
||||
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
|
||||
)
|
||||
|
||||
let surveys = []
|
||||
try {
|
||||
surveys = await $fetch('https://api.modrinth.com/v2/surveys')
|
||||
} catch (e) {
|
||||
console.error('Error fetching surveys:', e)
|
||||
}
|
||||
|
||||
const surveyToShow = surveys.find(
|
||||
(survey) =>
|
||||
!!(
|
||||
localStorage.getItem(`survey-${survey.id}-display`) === null &&
|
||||
survey.type === 'tally_app' &&
|
||||
((survey.condition === 'active_player' && isActivePlayer) ||
|
||||
(survey.assigned_users?.includes(userId) && !survey.dismissed_users?.includes(userId)))
|
||||
),
|
||||
)
|
||||
|
||||
if (surveyToShow) {
|
||||
availableSurvey.value = surveyToShow
|
||||
} else {
|
||||
console.info('No user survey to show')
|
||||
}
|
||||
}
|
||||
|
||||
provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
</script>
|
||||
|
||||
@@ -1544,12 +1515,12 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
class="flex items-center gap-4 text-contrast font-semibold text-xl select-none cursor-default"
|
||||
>
|
||||
<RefreshCwIcon data-tauri-drag-region class="animate-spin w-6 h-6" />
|
||||
Restarting...
|
||||
{{ formatMessage(messages.restarting) }}
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
<Suspense>
|
||||
<AppSettingsModal ref="settingsModal" />
|
||||
<AppSettingsModal ref="appSettingsModal" />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<ModrinthAccountRequiredModal ref="modrinthLoginModal" :request-auth="requestModrinthAuth" />
|
||||
@@ -1567,27 +1538,24 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
/>
|
||||
<UnknownPackWarningModal ref="unknownPackWarningModal" />
|
||||
<div
|
||||
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.5rem] w-[--left-bar-width]"
|
||||
class="app-grid-navbar bg-bg-raised flex flex-col p-[0.5rem] pt-0 gap-[0.25rem] w-[--left-bar-width]"
|
||||
>
|
||||
<NavButton v-tooltip.right="'Home'" to="/">
|
||||
<NavButton v-tooltip.right="formatMessage(messages.home)" to="/">
|
||||
<HomeIcon />
|
||||
</NavButton>
|
||||
<NavButton v-if="themeStore.featureFlags.worlds_tab" v-tooltip.right="'Worlds'" to="/worlds">
|
||||
<WorldIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Discover content'"
|
||||
v-tooltip.right="formatMessage(commonMessages.discoverContentLabel)"
|
||||
to="/browse/modpack"
|
||||
:is-primary="() => route.path.startsWith('/browse') && !route.query.i"
|
||||
:is-subpage="(route) => route.path.startsWith('/project') && !route.query.i"
|
||||
>
|
||||
<CompassIcon />
|
||||
</NavButton>
|
||||
<NavButton v-tooltip.right="'Skin selector'" to="/skins">
|
||||
<ChangeSkinIcon />
|
||||
<NavButton v-tooltip.right="formatMessage(appMessages.skinSelectorLabel)" to="/skins">
|
||||
<ShirtIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Library'"
|
||||
v-tooltip.right="formatMessage(messages.library)"
|
||||
to="/library"
|
||||
:is-primary="(r) => r.path === '/library' || r.path === '/library'"
|
||||
:is-subpage="
|
||||
@@ -1600,19 +1568,18 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<LibraryIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Modrinth Hosting'"
|
||||
v-tooltip.right="formatMessage(messages.modrinthHosting)"
|
||||
to="/hosting/manage"
|
||||
:is-primary="(r) => r.path === '/hosting/manage' || r.path === '/hosting/manage/'"
|
||||
:is-subpage="(r) => r.path.startsWith('/hosting/manage/') && r.path !== '/hosting/manage/'"
|
||||
>
|
||||
<ServerStackIcon />
|
||||
</NavButton>
|
||||
<div class="h-px w-6 mx-auto my-2 bg-surface-5"></div>
|
||||
<suspense>
|
||||
<QuickInstanceSwitcher />
|
||||
</suspense>
|
||||
<NavButton
|
||||
v-tooltip.right="'Create new instance'"
|
||||
v-tooltip.right="formatMessage(messages.createNewInstance)"
|
||||
:to="() => installationModal?.show()"
|
||||
:disabled="offline"
|
||||
>
|
||||
@@ -1621,82 +1588,106 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<div class="flex flex-grow"></div>
|
||||
<NavButton
|
||||
v-tooltip.right="formatMessage(commonMessages.settingsLabel)"
|
||||
:to="() => $refs.settingsModal.show()"
|
||||
:to="() => appSettingsModal?.show()"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</NavButton>
|
||||
<OverflowMenu
|
||||
<TeleportOverflowMenu
|
||||
v-if="credentials?.user"
|
||||
v-tooltip.right="`Modrinth account`"
|
||||
class="w-12 h-12 text-primary rounded-full flex items-center justify-center text-2xl transition-all bg-transparent hover:bg-button-bg hover:text-contrast border-0 cursor-pointer"
|
||||
v-tooltip.right="formatMessage(messages.modrinthAccount)"
|
||||
type="quiet"
|
||||
size="xl"
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'view-profile',
|
||||
action: () => openUrl('https://modrinth.com/user/' + credentials.user.username),
|
||||
label: formatMessage(messages.signedInAs, {
|
||||
username: credentials.user.username,
|
||||
}),
|
||||
action: () => router.push(`/user/${encodeURIComponent(credentials.user.username)}`),
|
||||
},
|
||||
{
|
||||
id: 'sign-out',
|
||||
label: formatMessage(commonMessages.signOutButton),
|
||||
tone: 'red',
|
||||
action: () => logOut(),
|
||||
color: 'danger',
|
||||
},
|
||||
]"
|
||||
placement="right-end"
|
||||
:distance="4"
|
||||
>
|
||||
<Avatar :src="credentials?.user?.avatar_url" alt="" size="32px" circle />
|
||||
<template #view-profile>
|
||||
<UserIcon />
|
||||
<span class="inline-flex items-center gap-1">
|
||||
Signed in as
|
||||
<span class="inline-flex items-center gap-1 text-contrast font-semibold">
|
||||
<Avatar :src="credentials?.user?.avatar_url" alt="" size="20px" circle />
|
||||
{{ credentials?.user?.username }}
|
||||
</span>
|
||||
<IntlFormatted
|
||||
:message-id="messages.signedInAs"
|
||||
:values="{ username: credentials?.user?.username }"
|
||||
>
|
||||
<template #user="{ children }">
|
||||
<span class="inline-flex items-center gap-1 text-contrast font-semibold">
|
||||
<Avatar :src="credentials?.user?.avatar_url" alt="" size="20px" circle />
|
||||
<component :is="() => children" />
|
||||
</span>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
<ExternalIcon />
|
||||
</template>
|
||||
<template #sign-out> <LogOutIcon /> Sign out </template>
|
||||
</OverflowMenu>
|
||||
<template #sign-out>
|
||||
<LogOutIcon />
|
||||
{{ formatMessage(commonMessages.signOutButton) }}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
<NavButton
|
||||
v-else
|
||||
v-tooltip.right="'Sign in to a Modrinth account'"
|
||||
v-tooltip.right="formatMessage(messages.signInToModrinthAccount)"
|
||||
:to="() => requestSignIn()"
|
||||
>
|
||||
<LogInIcon class="text-brand" />
|
||||
</NavButton>
|
||||
</div>
|
||||
<div data-tauri-drag-region class="app-grid-statusbar bg-bg-raised h-[--top-bar-height] flex">
|
||||
<div data-tauri-drag-region class="flex min-w-0 flex-1 overflow-hidden p-3">
|
||||
<ModrinthAppLogo class="h-full w-auto shrink-0 text-contrast pointer-events-none" />
|
||||
<div data-tauri-drag-region class="flex shrink-0 items-center gap-1 ml-3">
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
<div data-tauri-drag-region class="flex min-w-0 flex-1 items-center overflow-hidden p-2">
|
||||
<TextLogo class="h-7 w-auto shrink-0 text-contrast pointer-events-none" />
|
||||
<div data-tauri-drag-region class="ml-2 flex shrink-0 items-center gap-2">
|
||||
<IconButton
|
||||
type="outlined"
|
||||
label="Go back"
|
||||
class="!h-7 !min-w-7 !w-7 !border !border-surface-4 !p-0 !opacity-100"
|
||||
:disabled="!canNavigateBack"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
<ChevronLeftIcon
|
||||
class="!size-4 !text-primary"
|
||||
:class="{ 'opacity-20': !canNavigateBack }"
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="outlined"
|
||||
label="Go forward"
|
||||
class="!h-7 !min-w-7 !w-7 !border !border-surface-4 !p-0 !opacity-100"
|
||||
:disabled="!canNavigateForward"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
<ChevronRightIcon
|
||||
class="!size-4 !text-primary"
|
||||
:class="{ 'opacity-20': !canNavigateForward }"
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
<Breadcrumbs class="pt-[2px]" />
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<section data-tauri-drag-region class="flex shrink-0 ml-auto items-center">
|
||||
<ButtonStyled
|
||||
<IconButton
|
||||
v-if="!forceSidebar && themeStore.toggleSidebar"
|
||||
:type="sidebarToggled ? 'standard' : 'transparent'"
|
||||
circular
|
||||
:type="sidebarToggled ? 'base' : 'quiet'"
|
||||
label="Next image"
|
||||
class="mr-3 transition-transform"
|
||||
:class="{ 'rotate-180': !sidebarToggled }"
|
||||
@click="sidebarToggled = !sidebarToggled"
|
||||
>
|
||||
<button
|
||||
class="mr-3 transition-transform"
|
||||
:class="{ 'rotate-180': !sidebarToggled }"
|
||||
@click="sidebarToggled = !sidebarToggled"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<RightArrowIcon />
|
||||
</IconButton>
|
||||
<div class="flex mr-3">
|
||||
<Suspense>
|
||||
<AppActionBar />
|
||||
@@ -1715,28 +1706,7 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
}"
|
||||
>
|
||||
<div class="app-viewport flex-grow router-view">
|
||||
<transition name="popup-survey">
|
||||
<div
|
||||
v-if="availableSurvey"
|
||||
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-surface-5 border-[1px] border-solid border-b-0 p-4"
|
||||
>
|
||||
<h2 class="text-lg font-extrabold mt-0 mb-2">Hey there Modrinth user!</h2>
|
||||
<p class="m-0 leading-tight">
|
||||
Would you mind answering a few questions about your experience with Modrinth App?
|
||||
</p>
|
||||
<p class="mt-3 mb-4 leading-tight">
|
||||
This feedback will go directly to the Modrinth team and help guide future updates!
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="openSurvey"><NotepadTextIcon /> Take survey</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="dismissSurvey"><XIcon /> No thanks</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<SurveyPopup />
|
||||
<div
|
||||
class="loading-indicator-container h-8 fixed z-50 pointer-events-none"
|
||||
:style="{
|
||||
@@ -1800,7 +1770,9 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
<div id="sidebar-teleport-target" class="sidebar-teleport-content"></div>
|
||||
<div class="sidebar-default-content" :class="{ 'sidebar-enabled': sidebarVisible }">
|
||||
<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>
|
||||
<h3 class="text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.playingAs) }}
|
||||
</h3>
|
||||
<suspense>
|
||||
<AccountsCard ref="accounts" />
|
||||
</suspense>
|
||||
@@ -1815,18 +1787,26 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
|
||||
/>
|
||||
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
|
||||
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
|
||||
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">
|
||||
{{ formatMessage(messages.news) }}
|
||||
</h3>
|
||||
<div class="space-y-4 flex flex-col items-center w-full">
|
||||
<NewsArticleCard
|
||||
v-for="(item, index) in news"
|
||||
:key="`news-${index}`"
|
||||
:article="item"
|
||||
/>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<a href="https://modrinth.com/news" target="_blank" class="my-4">
|
||||
<NewspaperIcon /> View all news
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonLink
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
href="https://modrinth.com/news"
|
||||
target="_blank"
|
||||
class="my-4"
|
||||
>
|
||||
<NewspaperIcon />
|
||||
{{ formatMessage(messages.viewAllNews) }}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1837,7 +1817,8 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
class="absolute bottom-[250px] w-full flex justify-center items-center gap-1 px-4 py-3 text-purple font-medium hover:underline z-10"
|
||||
target="_blank"
|
||||
>
|
||||
<ArrowBigUpDashIcon class="text-2xl" /> Upgrade to Modrinth+
|
||||
<ArrowBigUpDashIcon class="text-2xl" />
|
||||
{{ formatMessage(messages.upgradeToModrinthPlus) }}
|
||||
</a>
|
||||
<PromotionWrapper />
|
||||
</template>
|
||||
@@ -2049,26 +2030,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.popup-survey-enter-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s cubic-bezier(0.51, 1.08, 0.35, 1.15);
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.popup-survey-leave-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s cubic-bezier(0.68, -0.17, 0.23, 0.11);
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.popup-survey-enter-from,
|
||||
.popup-survey-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10rem) scale(0.8) scaleY(1.6);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.nav-button-animated-enter-active {
|
||||
transition: all 0.5s cubic-bezier(0.15, 1.4, 0.64, 0.96);
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
class="flex flex-col gap-3 bg-button-bg border border-solid border-surface-5 rounded-xl p-3 mt-2"
|
||||
>
|
||||
<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>
|
||||
<Button type="colored" color="brand" :disabled="loginDisabled" @click="login()">
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
{{ formatMessage(messages.signInToMinecraft) }}
|
||||
</Button>
|
||||
</div>
|
||||
<Accordion
|
||||
v-else
|
||||
@@ -60,24 +58,28 @@
|
||||
{{ 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>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.removeAccount)"
|
||||
type="quiet"
|
||||
color="red"
|
||||
:label="formatMessage(messages.removeAccount)"
|
||||
class="mr-2 hover:!bg-red focus-visible:!bg-red hover:!text-[var(--color-accent-contrast)] focus-visible:!text-[var(--color-accent-contrast)]"
|
||||
@click="logout(account.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</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>
|
||||
<Button
|
||||
v-if="accounts.length > 0"
|
||||
class="w-full"
|
||||
:disabled="loginDisabled"
|
||||
@click="login()"
|
||||
>
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addAccount) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
@@ -95,8 +97,9 @@ import {
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
||||
import { Button, injectNotificationManager, TeleportOverflowMenu } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -36,27 +36,26 @@ const handleSearchContent = async () => {
|
||||
|
||||
<template>
|
||||
<div class="joined-buttons">
|
||||
<ButtonStyled>
|
||||
<button @click="handleSearchContent">
|
||||
<PlusIcon />
|
||||
Install content
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'from_file',
|
||||
action: handleAddContentFromFile,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<Button @click="handleSearchContent">
|
||||
<PlusIcon />
|
||||
Install content
|
||||
</Button>
|
||||
<TeleportOverflowMenu
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'from_file',
|
||||
label: 'Add from file',
|
||||
action: handleAddContentFromFile,
|
||||
},
|
||||
]"
|
||||
class="!w-auto !px-2.5 !rounded-xl"
|
||||
>
|
||||
<DropdownIcon />
|
||||
<template #from_file>
|
||||
<FolderOpenIcon />
|
||||
<span class="no-wrap"> Add from file </span>
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ButtonStyled
|
||||
<IconButton
|
||||
v-if="hasActiveLoadingBars && !hasVisibleActiveDownloadToasts"
|
||||
v-tooltip="formatMessage(messages.viewActiveDownloads)"
|
||||
type="quiet"
|
||||
color="brand"
|
||||
type="transparent"
|
||||
circular
|
||||
:label="formatMessage(messages.viewActiveDownloads)"
|
||||
@click="openDownloadToast()"
|
||||
>
|
||||
<button v-tooltip="formatMessage(messages.viewActiveDownloads)" @click="openDownloadToast()">
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
<div v-if="offline" class="flex items-center gap-1">
|
||||
<UnplugIcon class="text-secondary" />
|
||||
<span class="text-sm text-contrast"> {{ formatMessage(messages.offline) }} </span>
|
||||
@@ -36,17 +36,23 @@
|
||||
@show="showInstances = true"
|
||||
@hide="showInstances = false"
|
||||
>
|
||||
<ButtonStyled type="transparent" circular size="small">
|
||||
<button
|
||||
v-tooltip="
|
||||
showInstances
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
>
|
||||
<DropdownIcon :class="{ 'rotate-180': !!showInstances }" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="
|
||||
showInstances
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
class="!size-6"
|
||||
type="quiet"
|
||||
size="xs"
|
||||
:label="
|
||||
showInstances
|
||||
? formatMessage(messages.hideMoreRunningInstances)
|
||||
: formatMessage(messages.showMoreRunningInstances)
|
||||
"
|
||||
>
|
||||
<DropdownIcon :class="{ 'rotate-180': !!showInstances }" />
|
||||
</IconButton>
|
||||
<template #popper>
|
||||
<div class="flex w-[20rem] max-h-[24rem] flex-col gap-2 overflow-auto">
|
||||
<div
|
||||
@@ -125,8 +131,8 @@ import {
|
||||
TerminalSquareIcon,
|
||||
UnplugIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { IconButton } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
injectPopupNotificationManager,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
ref="outerRef"
|
||||
data-tauri-drag-region
|
||||
class="min-w-0 overflow-hidden pl-3"
|
||||
class="min-w-0 overflow-hidden pl-4"
|
||||
:class="{ 'breadcrumb-fade-mask': isOverflowing }"
|
||||
:style="isOverflowing ? { '--scroll-distance': `-${overflowAmount}px` } : undefined"
|
||||
@mouseenter="onMouseEnter"
|
||||
@@ -11,30 +11,48 @@
|
||||
<div
|
||||
ref="innerRef"
|
||||
data-tauri-drag-region
|
||||
class="flex w-fit items-center gap-1"
|
||||
class="flex w-fit items-center gap-2 pr-4"
|
||||
:class="{ 'breadcrumbs-scroll': isAnimating }"
|
||||
@animationiteration="onAnimationIteration"
|
||||
>
|
||||
{{ breadcrumbData.resetToNames(breadcrumbs) }}
|
||||
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name">
|
||||
<router-link
|
||||
v-if="breadcrumb.link"
|
||||
:to="{
|
||||
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id as string)),
|
||||
query: breadcrumb.query,
|
||||
}"
|
||||
class="shrink-0 whitespace-nowrap text-primary"
|
||||
<template v-for="(breadcrumb, index) in breadcrumbs" :key="breadcrumb.slot">
|
||||
<component
|
||||
:is="index < breadcrumbs.length - 1 && breadcrumb.to ? RouterLink : 'span'"
|
||||
v-bind="index < breadcrumbs.length - 1 && breadcrumb.to ? { to: breadcrumb.to } : {}"
|
||||
:data-tauri-drag-region="index === breadcrumbs.length - 1 ? '' : undefined"
|
||||
class="flex shrink-0 items-center gap-1.5 whitespace-nowrap text-base font-medium leading-6"
|
||||
:class="
|
||||
index === breadcrumbs.length - 1
|
||||
? 'cursor-default select-none text-contrast'
|
||||
: 'text-primary hover:text-contrast'
|
||||
"
|
||||
:aria-current="index === breadcrumbs.length - 1 ? 'page' : undefined"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</router-link>
|
||||
<span
|
||||
v-else
|
||||
<Avatar
|
||||
v-if="breadcrumb.visual?.type === 'image'"
|
||||
:src="breadcrumb.visual.src"
|
||||
:alt="breadcrumb.visual.alt ?? breadcrumb.label"
|
||||
:circle="breadcrumb.visual.circle"
|
||||
:tint-by="breadcrumb.visual.tintBy ?? breadcrumb.id"
|
||||
size="20px"
|
||||
no-shadow
|
||||
raised
|
||||
class="inline-block shrink-0 align-middle"
|
||||
:class="{ '!rounded-md': !breadcrumb.visual.circle }"
|
||||
/>
|
||||
<component
|
||||
:is="breadcrumb.visual.component"
|
||||
v-else-if="breadcrumb.visual?.type === 'icon'"
|
||||
class="size-5 shrink-0 text-primary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{{ breadcrumb.label }}</span>
|
||||
</component>
|
||||
<ChevronRightIcon
|
||||
v-if="index < breadcrumbs.length - 1"
|
||||
data-tauri-drag-region
|
||||
class="shrink-0 whitespace-nowrap text-contrast font-semibold cursor-default select-none"
|
||||
>
|
||||
{{ resolveLabel(breadcrumb.name) }}
|
||||
</span>
|
||||
<ChevronRightIcon v-if="breadcrumb.link" data-tauri-drag-region class="w-5 h-5 shrink-0" />
|
||||
class="size-5 shrink-0 text-primary"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,34 +60,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon } from '@modrinth/assets'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Avatar } from '@modrinth/ui'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { injectBreadcrumbManager } from '@/providers/breadcrumbs'
|
||||
|
||||
interface Breadcrumb {
|
||||
name: string
|
||||
link?: string
|
||||
query?: Record<string, string>
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbData = useBreadcrumbs()
|
||||
|
||||
const breadcrumbs = computed<Breadcrumb[]>(() => {
|
||||
const additionalContext =
|
||||
route.meta.useContext === true
|
||||
? breadcrumbData.context
|
||||
: route.meta.useRootContext === true
|
||||
? breadcrumbData.rootContext
|
||||
: null
|
||||
const crumbs = (route.meta.breadcrumb ?? []) as Breadcrumb[]
|
||||
return additionalContext ? [additionalContext as Breadcrumb, ...crumbs] : crumbs
|
||||
})
|
||||
|
||||
function resolveLabel(name: string): string {
|
||||
return name.charAt(0) === '?' ? breadcrumbData.getName(name.slice(1)) : name
|
||||
}
|
||||
const { entries: breadcrumbs } = injectBreadcrumbManager()
|
||||
|
||||
// Overflow detection
|
||||
const outerRef = ref<HTMLDivElement | null>(null)
|
||||
@@ -83,9 +80,13 @@ let stopping = false
|
||||
|
||||
function checkOverflow() {
|
||||
if (!outerRef.value || !innerRef.value) return
|
||||
const overflow = innerRef.value.scrollWidth - outerRef.value.clientWidth
|
||||
const outerStyles = window.getComputedStyle(outerRef.value)
|
||||
const horizontalPadding =
|
||||
Number.parseFloat(outerStyles.paddingLeft) + Number.parseFloat(outerStyles.paddingRight)
|
||||
const availableWidth = outerRef.value.clientWidth - horizontalPadding
|
||||
const overflow = innerRef.value.scrollWidth - availableWidth
|
||||
isOverflowing.value = overflow > 0
|
||||
overflowAmount.value = overflow + 12
|
||||
overflowAmount.value = overflow
|
||||
}
|
||||
|
||||
function onMouseEnter() {
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
WrenchIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
Collapsible,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { ChatIcon } from '@/assets/icons'
|
||||
@@ -273,12 +279,10 @@ async function copyToClipboard(text) {
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<a :href="supportLink" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="closable">
|
||||
<button @click="errorModal.hide()"><XIcon /> Close</button>
|
||||
</ButtonStyled>
|
||||
<ButtonLink :href="supportLink" @click="errorModal.hide()"
|
||||
><ChatIcon /> Get support</ButtonLink
|
||||
>
|
||||
<Button v-if="closable" @click="errorModal.hide()"><XIcon /> Close</Button>
|
||||
</div>
|
||||
<template v-if="hasDebugInfo">
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -307,16 +311,15 @@ async function copyToClipboard(text) {
|
||||
>
|
||||
{{ debugInfo }}
|
||||
</div>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="'Copy debug info'"
|
||||
:label="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
FileTreeSelect,
|
||||
@@ -286,18 +286,14 @@ function isExportCandidateDisabled(path) {
|
||||
</div>
|
||||
<template #actions>
|
||||
<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>
|
||||
<Button type="outlined" @click="exportModal.hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="exportPack">
|
||||
<PackageIcon />
|
||||
{{ formatMessage(messages.exportButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
StopCircleIcon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||
import { Avatar, IconButton, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
@@ -168,30 +168,37 @@ onUnmounted(() => unlisten())
|
||||
<span class="line-clamp-2">{{ instance.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ButtonStyled v-if="playing" color="red" circular @mousehover="checkProcess">
|
||||
<button v-tooltip="'Stop'" @click="(e) => stop(e, 'InstanceCard')">
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="modLoading" color="standard" circular>
|
||||
<button v-tooltip="'Instance is loading...'" disabled>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="!instance.quarantined"
|
||||
:color="first ? 'brand' : 'standard'"
|
||||
circular
|
||||
<IconButton
|
||||
v-if="playing"
|
||||
v-tooltip="'Stop'"
|
||||
type="colored"
|
||||
color="red"
|
||||
:label="'Stop'"
|
||||
@mouseenter="checkProcess"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<!-- Translate for optical centering -->
|
||||
<PlayIcon class="translate-x-[1px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<StopCircleIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="modLoading"
|
||||
v-tooltip="'Instance is loading...'"
|
||||
:label="'Instance is loading...'"
|
||||
disabled
|
||||
>
|
||||
<SpinnerIcon class="animate-spin" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="!instance.quarantined"
|
||||
v-tooltip="'Play'"
|
||||
:type="first ? 'colored' : 'base'"
|
||||
:color="first ? 'brand' : undefined"
|
||||
label="Play"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<!-- Translate for optical centering -->
|
||||
<PlayIcon class="translate-x-[1px]" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||
<TimerIcon />
|
||||
@@ -219,47 +226,51 @@ onUnmounted(() => unlisten())
|
||||
:class="`transition-all ${modLoading || installing ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<ButtonStyled v-if="playing" size="large" color="red" circular>
|
||||
<button
|
||||
v-tooltip="'Stop'"
|
||||
:class="{ 'scale-100 opacity-100': playing }"
|
||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-if="playing"
|
||||
v-tooltip="'Stop'"
|
||||
type="colored"
|
||||
color="red"
|
||||
size="xl"
|
||||
:label="'Stop'"
|
||||
:class="{ 'scale-100 opacity-100': playing }"
|
||||
class="transition-all scale-75 origin-bottom opacity-0 card-shadow"
|
||||
@click="(e) => stop(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
</IconButton>
|
||||
<SpinnerIcon
|
||||
v-else-if="modLoading || installing"
|
||||
v-tooltip="modLoading ? 'Instance is loading...' : 'Installing...'"
|
||||
class="animate-spin w-8 h-8"
|
||||
tabindex="-1"
|
||||
/>
|
||||
<ButtonStyled
|
||||
<IconButton
|
||||
v-else-if="!installed && !instance.quarantined"
|
||||
size="large"
|
||||
v-tooltip="'Repair'"
|
||||
type="colored"
|
||||
color="brand"
|
||||
circular
|
||||
size="xl"
|
||||
:label="'Repair'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => repair(e)"
|
||||
>
|
||||
<button
|
||||
v-tooltip="'Repair'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => repair(e)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="!instance.quarantined" size="large" color="brand" circular>
|
||||
<button
|
||||
v-tooltip="'Play'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mousehover="checkProcess"
|
||||
>
|
||||
<PlayIcon class="translate-x-[2px]" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-else-if="!instance.quarantined"
|
||||
v-tooltip="'Play'"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
:label="'Play'"
|
||||
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
|
||||
@click="(e) => play(e, 'InstanceCard')"
|
||||
@mouseenter="checkProcess"
|
||||
>
|
||||
<PlayIcon class="translate-x-[2px]" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, FormattedTag } from '@modrinth/ui'
|
||||
import { Avatar, ButtonLink, FormattedTag } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -48,9 +48,7 @@ const instanceLink = computed(() => {
|
||||
</span>
|
||||
</span>
|
||||
</router-link>
|
||||
<ButtonStyled>
|
||||
<router-link :to="instanceLink"> <LeftArrowIcon /> Back to instance </router-link>
|
||||
</ButtonStyled>
|
||||
<ButtonLink :to="instanceLink"> <LeftArrowIcon /> Back to instance </ButtonLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<ModalWrapper ref="detectJavaModal" header="Select java version" :show-ad-on-close="false">
|
||||
<ModalWrapper
|
||||
ref="detectJavaModal"
|
||||
:header="formatMessage(messages.title)"
|
||||
:show-ad-on-close="false"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Table :columns="javaInstallColumns" :data="chosenInstallOptions" row-key="path">
|
||||
<template #cell-version="{ value }">
|
||||
@@ -10,51 +14,95 @@
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex items-center justify-end">
|
||||
<ButtonStyled v-if="currentSelected.path === row.path">
|
||||
<button class="!shadow-none" disabled><CheckIcon /> Selected</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button class="!shadow-none" @click="setJavaInstall(row)"><PlusIcon /> Select</button>
|
||||
</ButtonStyled>
|
||||
<Button v-if="currentSelected.path === row.path" disabled>
|
||||
<CheckIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.selected) }}
|
||||
</Button>
|
||||
<Button v-else @click="setJavaInstall(row)">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.select) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty-state>
|
||||
<div class="p-4 text-secondary">No java installations found!</div>
|
||||
<div class="p-4 text-secondary">
|
||||
{{ formatMessage(messages.noInstallationsFound) }}
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="flex justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button
|
||||
class="!shadow-none !border-surface-4 !border"
|
||||
@click="$refs.detectJavaModal.hide()"
|
||||
>
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="outlined"
|
||||
class="!border-surface-4 !border"
|
||||
@click="$refs.detectJavaModal.hide()"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
<script setup>
|
||||
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, Table } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { Button, defineMessages, injectNotificationManager, Table, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { find_filtered_jres } from '@/helpers/jre.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'app.java-detection.title',
|
||||
defaultMessage: 'Select Java installation',
|
||||
},
|
||||
versionColumn: {
|
||||
id: 'app.java-detection.columns.version',
|
||||
defaultMessage: 'Version',
|
||||
},
|
||||
pathColumn: {
|
||||
id: 'app.java-detection.columns.path',
|
||||
defaultMessage: 'Path',
|
||||
},
|
||||
actionsColumn: {
|
||||
id: 'app.java-detection.columns.actions',
|
||||
defaultMessage: 'Actions',
|
||||
},
|
||||
selected: {
|
||||
id: 'app.java-detection.selected',
|
||||
defaultMessage: 'Selected',
|
||||
},
|
||||
select: {
|
||||
id: 'app.java-detection.select',
|
||||
defaultMessage: 'Select',
|
||||
},
|
||||
noInstallationsFound: {
|
||||
id: 'app.java-detection.no-installations-found',
|
||||
defaultMessage: 'No Java installations found.',
|
||||
},
|
||||
cancel: {
|
||||
id: 'app.java-detection.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
})
|
||||
|
||||
const chosenInstallOptions = ref([])
|
||||
const detectJavaModal = ref(null)
|
||||
const currentSelected = ref({})
|
||||
const javaInstallColumns = [
|
||||
{ key: 'version', label: 'Version', width: '9rem' },
|
||||
{ key: 'path', label: 'Path' },
|
||||
{ key: 'actions', label: 'Actions', align: 'right', width: '10rem' },
|
||||
]
|
||||
const javaInstallColumns = computed(() => [
|
||||
{ key: 'version', label: formatMessage(messages.versionColumn), width: '9rem' },
|
||||
{ key: 'path', label: formatMessage(messages.pathColumn) },
|
||||
{
|
||||
key: 'actions',
|
||||
label: formatMessage(messages.actionsColumn),
|
||||
align: 'right',
|
||||
width: '10rem',
|
||||
},
|
||||
])
|
||||
|
||||
defineExpose({
|
||||
show: async (version, currentSelectedJava) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
autocomplete="off"
|
||||
:disabled="props.disabled"
|
||||
:model-value="props.modelValue ? props.modelValue.path : ''"
|
||||
:placeholder="placeholder ?? '/path/to/java'"
|
||||
:placeholder="placeholder ?? formatMessage(messages.pathPlaceholder)"
|
||||
wrapper-class="installation-input"
|
||||
@update:model-value="
|
||||
(val) => {
|
||||
@@ -17,57 +17,73 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<ButtonStyled
|
||||
<Button
|
||||
type="quiet"
|
||||
:color="
|
||||
!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
: undefined
|
||||
"
|
||||
color-fill="text"
|
||||
:aria-label="formatMessage(messages.testJavaInstallation)"
|
||||
class="!text-[var(--legacy-button-color,var(--color-base))] [&>svg]:!text-[var(--legacy-button-color,var(--color-primary))]"
|
||||
:disabled="testingJava || props.disabled"
|
||||
:style="{
|
||||
'--legacy-button-color':
|
||||
(!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') &&
|
||||
(!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') !== 'standard'
|
||||
? `var(--color-${
|
||||
!hoveringTest && !testingJava
|
||||
? testingJavaSuccess === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
})`
|
||||
: undefined,
|
||||
}"
|
||||
@click="runTest(props.modelValue?.path)"
|
||||
@mouseenter="!props.disabled && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<button
|
||||
class="!shadow-none"
|
||||
:disabled="testingJava || props.disabled"
|
||||
@click="runTest(props.modelValue?.path)"
|
||||
@mouseenter="!props.disabled && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="testingJavaSuccess === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon v-else-if="testingJavaSuccess === true && !hoveringTest" class="h-4 w-4" />
|
||||
<XCircleIcon v-else-if="testingJavaSuccess !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="!props.disabled" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<span class="installation-buttons">
|
||||
<ButtonStyled v-if="props.version">
|
||||
<button
|
||||
v-tooltip="testingJavaSuccess === true ? 'Already installed' : undefined"
|
||||
class="!shadow-none"
|
||||
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
|
||||
@click="reinstallJava"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ installingJava ? 'Installing...' : 'Install recommended' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
v-if="props.version"
|
||||
v-tooltip="
|
||||
testingJavaSuccess === true ? formatMessage(messages.alreadyInstalled) : undefined
|
||||
"
|
||||
:disabled="props.disabled || installingJava || testingJavaSuccess === true"
|
||||
@click="reinstallJava"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{
|
||||
installingJava
|
||||
? formatMessage(messages.installing)
|
||||
: formatMessage(messages.installRecommended)
|
||||
}}
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="autoDetect">
|
||||
<SearchIcon />
|
||||
{{ formatMessage(messages.detect) }}
|
||||
</Button>
|
||||
<Button :disabled="props.disabled" @click="handleJavaFileInput()">
|
||||
<FolderSearchIcon />
|
||||
{{ formatMessage(messages.browse) }}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -82,7 +98,13 @@ import {
|
||||
SpinnerIcon,
|
||||
XCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import {
|
||||
Button,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
@@ -92,6 +114,38 @@ import { trackEvent } from '@/helpers/analytics'
|
||||
import { auto_install_java, find_filtered_jres, get_jre } from '@/helpers/jre.js'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
pathPlaceholder: {
|
||||
id: 'app.java-selector.path.placeholder',
|
||||
defaultMessage: '/path/to/java',
|
||||
},
|
||||
testJavaInstallation: {
|
||||
id: 'app.java-selector.test-installation',
|
||||
defaultMessage: 'Test Java installation',
|
||||
},
|
||||
alreadyInstalled: {
|
||||
id: 'app.java-selector.already-installed',
|
||||
defaultMessage: 'Already installed',
|
||||
},
|
||||
installing: {
|
||||
id: 'app.java-selector.installing',
|
||||
defaultMessage: 'Installing...',
|
||||
},
|
||||
installRecommended: {
|
||||
id: 'app.java-selector.install-recommended',
|
||||
defaultMessage: 'Install recommended',
|
||||
},
|
||||
detect: {
|
||||
id: 'app.java-selector.detect',
|
||||
defaultMessage: 'Detect',
|
||||
},
|
||||
browse: {
|
||||
id: 'app.java-selector.browse',
|
||||
defaultMessage: 'Browse',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { CheckIcon } from '@modrinth/assets'
|
||||
import { Badge, ButtonStyled } from '@modrinth/ui'
|
||||
import { Badge, IconButton } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
@@ -74,18 +74,16 @@ const onHide = () => {
|
||||
@click="$router.push(`/project/${version.project_id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<ButtonStyled
|
||||
circular
|
||||
:color="version.id === installedVersion ? 'standard' : 'brand'"
|
||||
<IconButton
|
||||
:type="version.id === installedVersion ? 'base' : 'colored'"
|
||||
:color="version.id === installedVersion ? undefined : 'brand'"
|
||||
label="Switch version"
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
>
|
||||
<button
|
||||
:disabled="inProgress || installing || version.id === installedVersion"
|
||||
@click.stop="() => switchVersion(version.id)"
|
||||
>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SwapIcon v-if="version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="name-cell table-cell table-text">
|
||||
<div class="version-link">
|
||||
|
||||
@@ -1,41 +1,151 @@
|
||||
<script setup>
|
||||
import { SpinnerIcon } from '@modrinth/assets'
|
||||
import { Avatar, injectNotificationManager } from '@modrinth/ui'
|
||||
import { Avatar, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import { list } from '@/helpers/instance'
|
||||
|
||||
const ITEM_SIZE = 52
|
||||
const APPROX_USED_VERTICAL_SPACE = 513 // doesn't need to be exact lol just close enough so there's a little gap and no overflow
|
||||
const STORAGE_KEY = 'modrinth-quick-instance-count'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const maxAuto = ref(0)
|
||||
const allInstances = ref([])
|
||||
const dragging = ref(false)
|
||||
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
const userLimit = ref(stored === null ? null : Number(stored))
|
||||
|
||||
const maxVisible = computed(() => Math.min(maxAuto.value, allInstances.value.length))
|
||||
const visibleCount = computed(() => Math.min(userLimit.value ?? maxVisible.value, maxVisible.value))
|
||||
const recentInstances = computed(() => allInstances.value.slice(0, visibleCount.value))
|
||||
const canDrag = computed(() => maxVisible.value > 0)
|
||||
const showOverdrag = ref(false)
|
||||
|
||||
const updateMaxAuto = () => {
|
||||
maxAuto.value = Math.max(
|
||||
0,
|
||||
Math.floor((window.innerHeight - APPROX_USED_VERTICAL_SPACE) / ITEM_SIZE),
|
||||
)
|
||||
}
|
||||
|
||||
const setLimit = (count) => {
|
||||
const clamped = Math.max(0, Math.min(count, maxVisible.value))
|
||||
if (clamped >= maxVisible.value) {
|
||||
userLimit.value = null
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
} else {
|
||||
userLimit.value = clamped
|
||||
localStorage.setItem(STORAGE_KEY, String(clamped))
|
||||
}
|
||||
}
|
||||
|
||||
let dragStartY = 0
|
||||
let dragStartCount = 0
|
||||
let wasOverdragging = false
|
||||
let overdragTimeout = null
|
||||
|
||||
const clearOverdragFlash = () => {
|
||||
showOverdrag.value = false
|
||||
if (overdragTimeout !== null) {
|
||||
clearTimeout(overdragTimeout)
|
||||
overdragTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
const flashOverdrag = () => {
|
||||
showOverdrag.value = true
|
||||
if (overdragTimeout !== null) {
|
||||
clearTimeout(overdragTimeout)
|
||||
}
|
||||
overdragTimeout = setTimeout(() => {
|
||||
showOverdrag.value = false
|
||||
overdragTimeout = null
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const onDividerPointerDown = (event) => {
|
||||
if (!canDrag.value) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
dragging.value = true
|
||||
wasOverdragging = false
|
||||
clearOverdragFlash()
|
||||
dragStartY = event.clientY
|
||||
dragStartCount = visibleCount.value
|
||||
document.body.classList.add('quick-instance-dragging')
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
}
|
||||
|
||||
const onDividerPointerMove = (event) => {
|
||||
if (!dragging.value) {
|
||||
return
|
||||
}
|
||||
const delta = event.clientY - dragStartY
|
||||
const target = dragStartCount + Math.round(delta / ITEM_SIZE)
|
||||
const isOverdragging = target < 0 || target > maxAuto.value
|
||||
if (isOverdragging && !wasOverdragging) {
|
||||
flashOverdrag()
|
||||
}
|
||||
wasOverdragging = isOverdragging
|
||||
setLimit(target)
|
||||
}
|
||||
|
||||
const endDrag = (event) => {
|
||||
if (!dragging.value) {
|
||||
return
|
||||
}
|
||||
dragging.value = false
|
||||
wasOverdragging = false
|
||||
clearOverdragFlash()
|
||||
document.body.classList.remove('quick-instance-dragging')
|
||||
if (event?.currentTarget?.hasPointerCapture?.(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
}
|
||||
|
||||
const onDividerPointerUp = (event) => {
|
||||
endDrag(event)
|
||||
}
|
||||
|
||||
const recentInstances = ref([])
|
||||
const getInstances = async () => {
|
||||
const instances = await list().catch(handleError)
|
||||
|
||||
recentInstances.value = instances
|
||||
.sort((a, b) => {
|
||||
const dateACreated = dayjs(a.created)
|
||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||
for (const instance of instances) {
|
||||
queryClient.setQueryData(['instances', 'summary', instance.id], instance)
|
||||
}
|
||||
|
||||
const dateBCreated = dayjs(b.created)
|
||||
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
||||
allInstances.value = instances.sort((a, b) => {
|
||||
const dateACreated = dayjs(a.created)
|
||||
const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0)
|
||||
|
||||
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
||||
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
||||
const dateBCreated = dayjs(b.created)
|
||||
const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0)
|
||||
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed
|
||||
const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
.slice(0, 3)
|
||||
if (dateA.isSame(dateB)) {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
}
|
||||
|
||||
await getInstances()
|
||||
updateMaxAuto()
|
||||
|
||||
const unlistenInstance = await instance_listener(async (event) => {
|
||||
if (event.event !== 'synced') {
|
||||
@@ -43,29 +153,157 @@ const unlistenInstance = await instance_listener(async (event) => {
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateMaxAuto)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateMaxAuto)
|
||||
document.body.classList.remove('quick-instance-dragging')
|
||||
clearOverdragFlash()
|
||||
unlistenInstance()
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
dragTooltip: {
|
||||
id: 'app.quick-instance-switcher.drag-tooltip',
|
||||
defaultMessage: 'Drag to resize',
|
||||
},
|
||||
dragShowTooltip: {
|
||||
id: 'app.quick-instance-switcher.drag-show-tooltip',
|
||||
defaultMessage: 'Drag to show recent instances',
|
||||
},
|
||||
})
|
||||
|
||||
const dividerTooltip = computed(() => {
|
||||
if (!canDrag.value || dragging.value) {
|
||||
return null
|
||||
}
|
||||
return formatMessage(visibleCount.value === 0 ? messages.dragShowTooltip : messages.dragTooltip)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="instance in recentInstances" :key="instance.id" v-tooltip.right="instance.name">
|
||||
<NavButton :to="`/instance/${encodeURIComponent(instance.id)}`" class="relative">
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
size="28px"
|
||||
:tint-by="instance.id"
|
||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div
|
||||
v-if="instance.install_stage !== 'installed'"
|
||||
class="absolute inset-0 flex items-center justify-center z-10 pointer-events-none"
|
||||
>
|
||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||
</div>
|
||||
</NavButton>
|
||||
<Transition name="top-divider">
|
||||
<div
|
||||
v-if="recentInstances.length > 0"
|
||||
class="top-divider flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<div class="h-px w-8 bg-surface-5 shrink-0"></div>
|
||||
</div>
|
||||
</Transition>
|
||||
<TransitionGroup name="quick-instance" tag="div" class="flex flex-col items-center">
|
||||
<div
|
||||
v-for="instance in recentInstances"
|
||||
:key="instance.id"
|
||||
v-tooltip.right="instance.name"
|
||||
class="quick-instance-item"
|
||||
>
|
||||
<NavButton :to="`/instance/${encodeURIComponent(instance.id)}`" class="relative">
|
||||
<Avatar
|
||||
:src="instance.icon_path ? convertFileSrc(instance.icon_path) : null"
|
||||
size="28px"
|
||||
:tint-by="instance.id"
|
||||
:class="`transition-all ${instance.install_stage !== 'installed' ? `brightness-[0.25] scale-[0.85]` : `group-hover:brightness-75`}`"
|
||||
/>
|
||||
<div
|
||||
v-if="instance.install_stage !== 'installed'"
|
||||
class="absolute inset-0 flex items-center justify-center z-10 pointer-events-none"
|
||||
>
|
||||
<SpinnerIcon class="animate-spin w-4 h-4" />
|
||||
</div>
|
||||
</NavButton>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
<div
|
||||
v-tooltip.right="dividerTooltip"
|
||||
class="flex items-center justify-center py-2 select-none"
|
||||
:class="canDrag ? 'cursor-ns-resize touch-none group' : ''"
|
||||
@pointerdown="onDividerPointerDown"
|
||||
@pointermove="onDividerPointerMove"
|
||||
@pointerup="onDividerPointerUp"
|
||||
@pointercancel="onDividerPointerUp"
|
||||
>
|
||||
<div
|
||||
class="h-px w-8 transition-colors duration-200"
|
||||
:class="
|
||||
showOverdrag ? 'bg-red' : canDrag ? 'bg-surface-5 group-hover:bg-secondary' : 'bg-surface-5'
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
<div v-if="recentInstances.length > 0" class="h-px w-6 mx-auto my-2 bg-divider"></div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.top-divider {
|
||||
height: calc(1rem + 1px);
|
||||
}
|
||||
|
||||
.top-divider-enter-active,
|
||||
.top-divider-leave-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
height 0.25s ease;
|
||||
}
|
||||
|
||||
.top-divider-enter-from,
|
||||
.top-divider-leave-to {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.quick-instance-item {
|
||||
height: 3rem;
|
||||
overflow: hidden;
|
||||
|
||||
& + & {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-instance-enter-active,
|
||||
.quick-instance-leave-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s ease,
|
||||
height 0.25s ease,
|
||||
margin-top 0.25s ease;
|
||||
}
|
||||
|
||||
.quick-instance-enter-from,
|
||||
.quick-instance-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
height: 0;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.top-divider-enter-active,
|
||||
.top-divider-leave-active,
|
||||
.quick-instance-enter-active,
|
||||
.quick-instance-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.top-divider-enter-from,
|
||||
.top-divider-leave-to {
|
||||
opacity: 1;
|
||||
height: calc(1rem + 1px);
|
||||
}
|
||||
|
||||
.quick-instance-enter-from,
|
||||
.quick-instance-leave-to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
height: 3rem;
|
||||
margin-top: unset !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
body.quick-instance-dragging,
|
||||
body.quick-instance-dragging * {
|
||||
cursor: ns-resize !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import { NotepadTextIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button, defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { $fetch } from 'ofetch'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { get as getCreds } from '@/helpers/mr_auth.ts'
|
||||
|
||||
type Survey = {
|
||||
id: string
|
||||
tally_id: string
|
||||
type: string
|
||||
condition?: string
|
||||
assigned_users?: string[]
|
||||
dismissed_users?: string[]
|
||||
}
|
||||
|
||||
type TallyApi = {
|
||||
openPopup: (formId: string, options: object) => void
|
||||
}
|
||||
|
||||
const tallyWindow = window as Window & { Tally?: TallyApi }
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const availableSurvey = ref<Survey | null>(null)
|
||||
|
||||
const messages = defineMessages({
|
||||
surveyTitle: {
|
||||
id: 'app.survey.title',
|
||||
defaultMessage: 'Hey there Modrinth user!',
|
||||
},
|
||||
surveyBody: {
|
||||
id: 'app.survey.body',
|
||||
defaultMessage:
|
||||
'Would you mind answering a few questions about your experience with Modrinth App?',
|
||||
},
|
||||
surveyFooter: {
|
||||
id: 'app.survey.footer',
|
||||
defaultMessage:
|
||||
'This feedback will go directly to the Modrinth team and help guide future updates!',
|
||||
},
|
||||
takeSurvey: {
|
||||
id: 'app.survey.take-survey',
|
||||
defaultMessage: 'Take survey',
|
||||
},
|
||||
surveyNoThanks: {
|
||||
id: 'app.survey.no-thanks',
|
||||
defaultMessage: 'No thanks',
|
||||
},
|
||||
})
|
||||
|
||||
function cleanupOldSurveyDisplayData() {
|
||||
const threeWeeksAgo = new Date()
|
||||
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21)
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
|
||||
if (key?.startsWith('survey-') && key.endsWith('-display')) {
|
||||
const dateValue = new Date(localStorage.getItem(key) ?? '')
|
||||
if (dateValue < threeWeeksAgo) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openSurvey() {
|
||||
if (!availableSurvey.value) {
|
||||
console.error('No survey to open')
|
||||
return
|
||||
}
|
||||
|
||||
const creds = await getCreds().catch(handleError)
|
||||
const userId = creds?.user_id
|
||||
|
||||
const formId = availableSurvey.value.tally_id
|
||||
|
||||
const popupOptions = {
|
||||
layout: 'modal',
|
||||
width: 700,
|
||||
autoClose: 2000,
|
||||
hideTitle: true,
|
||||
hiddenFields: {
|
||||
user_id: userId,
|
||||
},
|
||||
onOpen: () => console.info('Opened user survey'),
|
||||
onClose: () => {
|
||||
console.info('Closed user survey')
|
||||
show_ads_window()
|
||||
},
|
||||
onSubmit: () => console.info('Active user survey submitted'),
|
||||
}
|
||||
|
||||
try {
|
||||
hide_ads_window()
|
||||
if (tallyWindow.Tally?.openPopup) {
|
||||
console.info(`Opening Tally popup for user survey (form ID: ${formId})`)
|
||||
dismissSurvey()
|
||||
tallyWindow.Tally.openPopup(formId, popupOptions)
|
||||
} else {
|
||||
console.warn('Tally script not yet loaded')
|
||||
show_ads_window()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error opening Tally popup:', e)
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
console.info(`Found user survey to show with tally_id: ${formId}`)
|
||||
tallyWindow.Tally?.openPopup(formId, popupOptions)
|
||||
}
|
||||
|
||||
function dismissSurvey() {
|
||||
if (!availableSurvey.value) return
|
||||
localStorage.setItem(`survey-${availableSurvey.value.id}-display`, String(new Date()))
|
||||
availableSurvey.value = null
|
||||
}
|
||||
|
||||
async function processPendingSurveys() {
|
||||
function isWithinLastTwoWeeks(date: string | Date | null | undefined) {
|
||||
if (!date) return false
|
||||
const twoWeeksAgo = new Date()
|
||||
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14)
|
||||
return new Date(date) >= twoWeeksAgo
|
||||
}
|
||||
|
||||
cleanupOldSurveyDisplayData()
|
||||
|
||||
const creds = await getCreds().catch(handleError)
|
||||
const userId = creds?.user_id
|
||||
|
||||
const instances = (await list().catch(handleError)) ?? []
|
||||
const isActivePlayer = instances.some(
|
||||
(instance) =>
|
||||
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
|
||||
)
|
||||
|
||||
let surveys: Survey[] = []
|
||||
try {
|
||||
surveys = await $fetch('https://api.modrinth.com/v2/surveys')
|
||||
} catch (e) {
|
||||
console.error('Error fetching surveys:', e)
|
||||
}
|
||||
|
||||
const surveyToShow = surveys.find(
|
||||
(survey) =>
|
||||
!!(
|
||||
localStorage.getItem(`survey-${survey.id}-display`) === null &&
|
||||
survey.type === 'tally_app' &&
|
||||
((survey.condition === 'active_player' && isActivePlayer) ||
|
||||
(!!userId &&
|
||||
survey.assigned_users?.includes(userId) &&
|
||||
!survey.dismissed_users?.includes(userId)))
|
||||
),
|
||||
)
|
||||
|
||||
if (surveyToShow) {
|
||||
availableSurvey.value = surveyToShow
|
||||
} else {
|
||||
console.info('No user survey to show')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const osType = await type()
|
||||
if (osType === 'windows') {
|
||||
await processPendingSurveys()
|
||||
} else {
|
||||
console.info('Skipping user surveys on non-Windows platforms')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="popup-survey">
|
||||
<div
|
||||
v-if="availableSurvey"
|
||||
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-surface-5 border-[1px] border-solid border-b-0 p-4"
|
||||
>
|
||||
<h2 class="text-lg font-extrabold mt-0 mb-2">
|
||||
{{ formatMessage(messages.surveyTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.surveyBody) }}
|
||||
</p>
|
||||
<p class="mt-3 mb-4 leading-tight">
|
||||
{{ formatMessage(messages.surveyFooter) }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<Button type="colored" color="brand" @click="openSurvey">
|
||||
<NotepadTextIcon />
|
||||
{{ formatMessage(messages.takeSurvey) }}
|
||||
</Button>
|
||||
<Button @click="dismissSurvey">
|
||||
<XIcon />
|
||||
{{ formatMessage(messages.surveyNoThanks) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.popup-survey-enter-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s cubic-bezier(0.51, 1.08, 0.35, 1.15);
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.popup-survey-leave-active {
|
||||
transition:
|
||||
opacity 0.25s ease,
|
||||
transform 0.25s cubic-bezier(0.68, -0.17, 0.23, 0.11);
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.popup-survey-enter-from,
|
||||
.popup-survey-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10rem) scale(0.8) scaleY(1.6);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ButtonStyled, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { Button, injectNotificationManager, ProjectCard } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
@@ -69,9 +69,7 @@ async function install() {
|
||||
</p>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="install">Install</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" @click="install">Install</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,34 +4,38 @@
|
||||
class="flex items-center gap-2 mr-1.5"
|
||||
data-tauri-drag-region-exclude
|
||||
>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button class="relative expanded-button" @click="() => getCurrentWindow().minimize()">
|
||||
<MinimizeIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent" circular>
|
||||
<button class="relative expanded-button" @click="() => getCurrentWindow().toggleMaximize()">
|
||||
<RestoreIcon v-if="isMaximized" />
|
||||
<MaximizeIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
color="red"
|
||||
color-fill="none"
|
||||
hover-color-fill="background"
|
||||
circular
|
||||
<IconButton
|
||||
type="quiet"
|
||||
label="Minimize window"
|
||||
class="relative expanded-button"
|
||||
@click="() => getCurrentWindow().minimize()"
|
||||
>
|
||||
<button class="relative expanded-button close-button" @click="handleClose">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<MinimizeIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="quiet"
|
||||
label="Toggle maximize window"
|
||||
class="relative expanded-button"
|
||||
@click="() => getCurrentWindow().toggleMaximize()"
|
||||
>
|
||||
<RestoreIcon v-if="isMaximized" />
|
||||
<MaximizeIcon v-else />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="quiet"
|
||||
color="red"
|
||||
label="Close window"
|
||||
class="relative expanded-button close-button hover:!bg-red focus-visible:!bg-red"
|
||||
@click="handleClose"
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { MaximizeIcon, MinimizeIcon, RestoreIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { IconButton } from '@modrinth/ui'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
<template>
|
||||
<ButtonStyled color="brand" type="outlined" hover-color-fill="background">
|
||||
<button
|
||||
v-if="showUpdatePill"
|
||||
type="button"
|
||||
class="!h-[34px] text-sm !transition-[opacity,transform,background-color,color,filter] !duration-200 ease-out"
|
||||
:class="{
|
||||
'opacity-0 scale-[0.96]': finishedDownloading && !animateReadyPill,
|
||||
'opacity-100 scale-100': finishedDownloading && animateReadyPill,
|
||||
}"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||
</span>
|
||||
<span v-else>{{ updateLabel }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
v-if="showUpdatePill"
|
||||
type="outlined"
|
||||
native-type="button"
|
||||
class="!h-[34px] text-sm !transition-[opacity,transform,background-color,color,filter] !duration-200 ease-out !text-brand [&>svg]:!text-brand !shadow-[inset_0_0_0_1px_var(--color-brand)] hover:!bg-brand focus-visible:!bg-brand hover:!text-[var(--color-accent-contrast)] focus-visible:!text-[var(--color-accent-contrast)]"
|
||||
:class="{
|
||||
'opacity-0 scale-[0.96]': finishedDownloading && !animateReadyPill,
|
||||
'opacity-100 scale-100': finishedDownloading && animateReadyPill,
|
||||
}"
|
||||
:disabled="isUpdateDownloading"
|
||||
:aria-busy="isUpdateDownloading"
|
||||
@click="handleUpdateClick"
|
||||
>
|
||||
<RefreshCwIcon v-if="finishedDownloading" :class="{ 'animate-spin': restarting }" />
|
||||
<DownloadIcon v-else />
|
||||
<span v-if="isUpdateDownloading">
|
||||
{{ formatMessage(messages.downloadingUpdate) }}
|
||||
<span class="inline-block w-[3ch] text-right tabular-nums">{{ downloadPercent }}%</span>
|
||||
</span>
|
||||
<span v-else>{{ updateLabel }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, RefreshCwIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { Button, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import { MailIcon, SearchIcon, SendIcon, UserIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
StyledInput,
|
||||
@@ -197,26 +198,20 @@ const messages = defineMessages({
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<template v-if="friend.id === userCredentials?.user_id">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="addFriend(friend)">
|
||||
<UserPlusIcon />
|
||||
Accept
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Ignore
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" @click="addFriend(friend)">
|
||||
<UserPlusIcon />
|
||||
Accept
|
||||
</Button>
|
||||
<Button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Ignore
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled>
|
||||
<button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,26 +235,28 @@ const messages = defineMessages({
|
||||
wrapper-class="flex-1"
|
||||
@keyup.enter="addFriendFromModal"
|
||||
/>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="username.length === 0" @click="addFriendFromModal">
|
||||
<SendIcon />
|
||||
{{ formatMessage(messages.sendFriendRequest) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="username.length === 0"
|
||||
@click="addFriendFromModal"
|
||||
>
|
||||
<SendIcon />
|
||||
{{ formatMessage(messages.sendFriendRequest) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<div v-if="userCredentials && !loading" class="flex gap-1 items-center mb-3 -ml-1">
|
||||
<template v-if="sortedFriends.length > 0">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.addFriend)"
|
||||
:aria-label="formatMessage(messages.addFriend)"
|
||||
@click="addFriendModal.show"
|
||||
>
|
||||
<UserPlusIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.addFriend)"
|
||||
type="quiet"
|
||||
:label="formatMessage(messages.addFriend)"
|
||||
@click="addFriendModal.show"
|
||||
>
|
||||
<UserPlusIcon />
|
||||
</IconButton>
|
||||
<StyledInput
|
||||
v-model="search"
|
||||
:icon="SearchIcon"
|
||||
@@ -274,23 +271,23 @@ const messages = defineMessages({
|
||||
<h3 v-else class="w-full text-base text-primary font-medium m-0">
|
||||
{{ formatMessage(messages.friends) }}
|
||||
</h3>
|
||||
<ButtonStyled v-if="incomingRequests.length > 0" circular type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
class="relative"
|
||||
:aria-label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
@click="friendInvitesModal.show"
|
||||
<IconButton
|
||||
v-if="incomingRequests.length > 0"
|
||||
v-tooltip="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
type="quiet"
|
||||
:label="formatMessage(messages.viewFriendRequests, { count: incomingRequests.length })"
|
||||
class="relative"
|
||||
@click="friendInvitesModal.show"
|
||||
>
|
||||
<MailIcon />
|
||||
<span
|
||||
v-if="incomingRequests.length > 0"
|
||||
aria-hidden="true"
|
||||
class="absolute bg-brand text-brand-inverted text-[8px] top-0.5 px-1 right-0.5 min-w-3 h-3 rounded-full flex items-center justify-center font-bold"
|
||||
>
|
||||
<MailIcon />
|
||||
<span
|
||||
v-if="incomingRequests.length > 0"
|
||||
aria-hidden="true"
|
||||
class="absolute bg-brand text-brand-inverted text-[8px] top-0.5 px-1 right-0.5 min-w-3 h-3 rounded-full flex items-center justify-center font-bold"
|
||||
>
|
||||
{{ incomingRequests.length }}
|
||||
</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
{{ incomingRequests.length }}
|
||||
</span>
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 v-if="loading" class="text-base text-primary font-medium m-0">
|
||||
|
||||
@@ -3,18 +3,19 @@ import { MoreVerticalIcon, TrashIcon, UserIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
defineMessages,
|
||||
OverflowMenu,
|
||||
IconButton,
|
||||
TeleportOverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type { FriendWithUserData } from '@/helpers/friends.ts'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -54,7 +55,7 @@ function createContextMenuOptions(friend: FriendWithUserData) {
|
||||
}
|
||||
|
||||
function openProfile(username: string) {
|
||||
openUrl('https://modrinth.com/user/' + username)
|
||||
void router.push(`/user/${encodeURIComponent(username)}`)
|
||||
}
|
||||
|
||||
const friendOptions = useTemplateRef('friendOptions')
|
||||
@@ -122,68 +123,71 @@ const messages = defineMessages({
|
||||
<div
|
||||
v-for="friend in friends"
|
||||
:key="friend.username"
|
||||
class="group grid items-center grid-cols-[auto_1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
|
||||
class="group grid items-center grid-cols-[1fr_auto] gap-2 hover:bg-button-bg transition-colors rounded-full mr-1"
|
||||
@contextmenu.prevent.stop="
|
||||
(event) => friendOptions?.showMenu(event, friend, createContextMenuOptions(friend))
|
||||
"
|
||||
>
|
||||
<div class="relative">
|
||||
<Avatar
|
||||
:src="friend.avatar"
|
||||
:class="{ grayscale: !friend.online && friend.accepted }"
|
||||
class="w-12 h-12 rounded-full"
|
||||
size="32px"
|
||||
circle
|
||||
/>
|
||||
<span
|
||||
v-if="friend.online"
|
||||
aria-hidden="true"
|
||||
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm m-0"
|
||||
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
{{ friend.username }}
|
||||
</span>
|
||||
<span v-if="!friend.accepted" class="m-0 text-xs">
|
||||
{{ formatMessage(messages.friendRequestSent) }}
|
||||
</span>
|
||||
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||
</div>
|
||||
<ButtonStyled v-if="friend.accepted" circular type="transparent">
|
||||
<OverflowMenu
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:options="[
|
||||
{
|
||||
id: 'view-profile',
|
||||
action: () => openProfile(friend.username),
|
||||
},
|
||||
{
|
||||
id: 'remove-friend',
|
||||
action: () => removeFriend(friend),
|
||||
color: 'red',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #view-profile>
|
||||
<UserIcon />
|
||||
{{ formatMessage(messages.viewProfile) }}
|
||||
</template>
|
||||
<template #remove-friend>
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.removeFriend) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else type="transparent" circular>
|
||||
<button v-tooltip="formatMessage(messages.cancelRequest)" @click="removeFriend(friend)">
|
||||
<XIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<RouterLink
|
||||
:to="`/user/${encodeURIComponent(friend.username)}`"
|
||||
class="grid min-w-0 grid-cols-[auto_1fr] items-center gap-2 text-inherit no-underline"
|
||||
>
|
||||
<div class="relative">
|
||||
<Avatar
|
||||
:src="friend.avatar"
|
||||
:class="{ grayscale: !friend.online && friend.accepted }"
|
||||
class="w-12 h-12 rounded-full"
|
||||
size="32px"
|
||||
circle
|
||||
/>
|
||||
<span
|
||||
v-if="friend.online"
|
||||
aria-hidden="true"
|
||||
class="bottom-[2px] right-[-2px] absolute w-3 h-3 bg-brand border-2 border-black border-solid rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span
|
||||
class="text-sm m-0"
|
||||
:class="friend.online || !friend.accepted ? 'text-contrast' : 'text-primary'"
|
||||
>
|
||||
{{ friend.username }}
|
||||
</span>
|
||||
<span v-if="!friend.accepted" class="m-0 text-xs">
|
||||
{{ formatMessage(messages.friendRequestSent) }}
|
||||
</span>
|
||||
<span v-else-if="friend.status" class="m-0 text-xs">{{ friend.status }}</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<TeleportOverflowMenu
|
||||
v-if="friend.accepted"
|
||||
type="quiet"
|
||||
label="More options"
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:options="[
|
||||
{
|
||||
id: 'remove-friend',
|
||||
label: formatMessage(messages.removeFriend),
|
||||
action: () => removeFriend(friend),
|
||||
tone: 'red',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
<template #remove-friend>
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.removeFriend) }}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
<IconButton
|
||||
v-else
|
||||
v-tooltip="formatMessage(messages.cancelRequest)"
|
||||
type="quiet"
|
||||
:label="formatMessage(messages.cancelRequest)"
|
||||
@click="removeFriend(friend)"
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<script setup>
|
||||
import { CheckIcon, PlusIcon, SearchIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
StyledInput,
|
||||
} from '@modrinth/ui'
|
||||
import { Admonition, Avatar, Button, injectNotificationManager, StyledInput } from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -109,19 +103,15 @@ async function addServer(instance) {
|
||||
/>
|
||||
{{ instance.name }}
|
||||
</router-link>
|
||||
<ButtonStyled>
|
||||
<button :disabled="instance.added || instance.adding" @click="addServer(instance)">
|
||||
<PlusIcon v-if="!instance.added && !instance.adding" />
|
||||
<CheckIcon v-else-if="instance.added" />
|
||||
{{ instance.adding ? 'Adding...' : instance.added ? 'Added' : 'Add' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button :disabled="instance.added || instance.adding" @click="addServer(instance)">
|
||||
<PlusIcon v-if="!instance.added && !instance.adding" />
|
||||
<CheckIcon v-else-if="instance.added" />
|
||||
{{ instance.adding ? 'Adding...' : instance.added ? 'Added' : 'Add' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group push-right">
|
||||
<ButtonStyled>
|
||||
<button @click="modal.hide()">Cancel</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="modal.hide()">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
</PageHeaderBadgeItem>
|
||||
<PageHeaderBadgeItem
|
||||
v-else
|
||||
:icon="UnknownIcon"
|
||||
:tooltip="sharedInstanceTooltip"
|
||||
aria-label="Shared instance information"
|
||||
class="!border-blue !bg-highlight-blue !text-blue"
|
||||
>
|
||||
Shared
|
||||
<UnknownIcon class="block size-4 shrink-0 text-current" aria-hidden="true" />
|
||||
</PageHeaderBadgeItem>
|
||||
</template>
|
||||
|
||||
@@ -85,66 +85,98 @@
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<ButtonStyled v-if="isInstalling" color="brand" size="large">
|
||||
<button type="button" disabled>
|
||||
{{ formatMessage(commonMessages.installingLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="playing" color="red" size="large">
|
||||
<button type="button" :disabled="stopping" @click="emit('stop')">
|
||||
<StopCircleIcon />
|
||||
{{
|
||||
stopping ? formatMessage(messages.stopping) : formatMessage(commonMessages.stopButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="instance.quarantined" color="brand" size="large">
|
||||
<button v-tooltip="formatMessage(messages.lockedPlayTooltip)" type="button" disabled>
|
||||
<PlayIcon />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else-if="instance.install_stage !== 'installed'" color="brand" size="large">
|
||||
<button type="button" @click="emit('repair')">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.repair) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<JoinedButtons
|
||||
v-else-if="!loading && isServerInstance"
|
||||
:actions="serverPlayActions"
|
||||
<Button
|
||||
v-if="isInstalling"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="large"
|
||||
/>
|
||||
<ButtonStyled v-else-if="!loading" color="brand" size="large">
|
||||
<button type="button" @click="emit('play')">
|
||||
<PlayIcon />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else color="brand" size="large">
|
||||
<button type="button" disabled>{{ formatMessage(messages.starting) }}</button>
|
||||
</ButtonStyled>
|
||||
size="xl"
|
||||
native-type="button"
|
||||
disabled
|
||||
>
|
||||
{{ formatMessage(commonMessages.installingLabel) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="playing"
|
||||
type="colored"
|
||||
color="red"
|
||||
size="xl"
|
||||
native-type="button"
|
||||
:disabled="stopping"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<StopCircleIcon />
|
||||
{{
|
||||
stopping ? formatMessage(messages.stopping) : formatMessage(commonMessages.stopButton)
|
||||
}}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="instance.quarantined"
|
||||
v-tooltip="formatMessage(messages.lockedPlayTooltip)"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
native-type="button"
|
||||
disabled
|
||||
>
|
||||
<PlayIcon />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="instance.install_stage !== 'installed'"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
native-type="button"
|
||||
@click="emit('repair')"
|
||||
>
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.repair) }}
|
||||
</Button>
|
||||
<SplitButton
|
||||
v-else-if="!loading && isServerInstance"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
:options="serverPlayOptions"
|
||||
:menu-label="formatMessage(messages.launchInstance)"
|
||||
@click="emit('playServer')"
|
||||
>
|
||||
<PlayIcon />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</SplitButton>
|
||||
<Button
|
||||
v-else-if="!loading"
|
||||
type="colored"
|
||||
color="brand"
|
||||
size="xl"
|
||||
native-type="button"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<PlayIcon />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</Button>
|
||||
<Button v-else type="colored" color="brand" size="xl" native-type="button" disabled>{{
|
||||
formatMessage(messages.starting)
|
||||
}}</Button>
|
||||
|
||||
<ButtonStyled circular size="large">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.instanceSettings)"
|
||||
type="button"
|
||||
:aria-label="formatMessage(messages.instanceSettings)"
|
||||
@click="emit('settings')"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="moreActions"
|
||||
:tooltip="formatMessage(messages.moreActions)"
|
||||
:aria-label="formatMessage(messages.moreActions)"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.instanceSettings)"
|
||||
size="xl"
|
||||
:label="formatMessage(messages.instanceSettings)"
|
||||
native-type="button"
|
||||
@click="emit('settings')"
|
||||
>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
<TeleportOverflowMenu
|
||||
type="quiet"
|
||||
size="xl"
|
||||
:label="formatMessage(messages.moreActions)"
|
||||
:tooltip="formatMessage(messages.moreActions)"
|
||||
:options="moreActions"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</PageHeaderActions>
|
||||
</template>
|
||||
</PageHeader>
|
||||
@@ -167,27 +199,24 @@ import {
|
||||
TimerIcon,
|
||||
UnknownIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, IconButton, SplitButton, TeleportOverflowMenu } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
formatLoaderLabel,
|
||||
type JoinedButtonAction,
|
||||
JoinedButtons,
|
||||
LoaderIcon as ServerLoaderIcon,
|
||||
type OverflowMenuOption,
|
||||
PageHeader,
|
||||
PageHeaderActions,
|
||||
PageHeaderBadgeItem,
|
||||
PageHeaderMetadata,
|
||||
PageHeaderMetadataItem,
|
||||
type ServerLoader,
|
||||
TeleportOverflowMenu,
|
||||
type TeleportOverflowMenuItem,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
@@ -247,6 +276,7 @@ const messages = defineMessages({
|
||||
defaultMessage: "This instance's content is being shared to other users.",
|
||||
},
|
||||
})
|
||||
const router = useRouter()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -331,7 +361,7 @@ const sharedInstanceManagerLabel = computed(() =>
|
||||
const sharedInstanceManagerAction = computed(() => {
|
||||
const manager = props.sharedInstanceManager
|
||||
if (manager?.type !== 'user') return undefined
|
||||
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(manager.name)}`)
|
||||
return () => router.push(`/user/${encodeURIComponent(manager.name)}`)
|
||||
})
|
||||
const playtimeLabel = computed(() => {
|
||||
if (props.timePlayed <= 0) return formatMessage(messages.neverPlayed)
|
||||
@@ -349,13 +379,7 @@ const playtimeLabel = computed(() => {
|
||||
const seconds = Math.floor(props.timePlayed)
|
||||
return `${seconds} second${seconds > 1 ? 's' : ''}`
|
||||
})
|
||||
const serverPlayActions = computed<JoinedButtonAction[]>(() => [
|
||||
{
|
||||
id: 'join_server',
|
||||
label: formatMessage(commonMessages.playButton),
|
||||
icon: PlayIcon,
|
||||
action: () => emit('playServer'),
|
||||
},
|
||||
const serverPlayOptions = computed<OverflowMenuOption[]>(() => [
|
||||
{
|
||||
id: 'launch_instance',
|
||||
label: formatMessage(messages.launchInstance),
|
||||
@@ -363,8 +387,8 @@ const serverPlayActions = computed<JoinedButtonAction[]>(() => [
|
||||
action: () => emit('play'),
|
||||
},
|
||||
])
|
||||
const moreActions = computed<TeleportOverflowMenuItem[]>(() => {
|
||||
const actions: TeleportOverflowMenuItem[] = [
|
||||
const moreActions = computed<OverflowMenuOption[]>(() => {
|
||||
const actions: OverflowMenuOption[] = [
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: formatMessage(messages.openFolder),
|
||||
@@ -392,12 +416,12 @@ const moreActions = computed<TeleportOverflowMenuItem[]>(() => {
|
||||
|
||||
if (props.instance.shared_instance?.role === 'member') {
|
||||
actions.push(
|
||||
{ divider: true },
|
||||
{ type: 'divider' },
|
||||
{
|
||||
id: 'report-shared-instance',
|
||||
label: formatMessage(commonMessages.reportButton),
|
||||
icon: ReportIcon,
|
||||
color: 'red',
|
||||
tone: 'red',
|
||||
action: (event) => emit('report', event),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
:instance="instance"
|
||||
@published="emit('published')"
|
||||
/>
|
||||
<InstanceAdmonitionsSharedInstanceUpdateAvailable
|
||||
v-else-if="item.kind === 'shared-instance-update-available'"
|
||||
:instance-name="instance.name"
|
||||
@review="emit('review-update', $event)"
|
||||
/>
|
||||
<InstanceAdmonitionsSharedInstanceWrongAccount
|
||||
v-else-if="item.kind === 'shared-instance-wrong-account'"
|
||||
:expected-user-id="sharedInstanceExpectedUserId"
|
||||
@@ -33,6 +38,7 @@ import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
import InstanceAdmonitionsSharedInstanceStale from './instance-admonitions-shared-instance-stale.vue'
|
||||
import InstanceAdmonitionsSharedInstanceUnavailable from './instance-admonitions-shared-instance-unavailable.vue'
|
||||
import InstanceAdmonitionsSharedInstanceUpdateAvailable from './instance-admonitions-shared-instance-update-available.vue'
|
||||
import InstanceAdmonitionsSharedInstanceWrongAccount from './instance-admonitions-shared-instance-wrong-account.vue'
|
||||
import type { InstanceAdmonitionItem, SharedInstanceRole } from './types'
|
||||
|
||||
@@ -48,11 +54,13 @@ const props = defineProps<{
|
||||
sharedInstanceExpectedUserId?: string | null
|
||||
sharedInstanceRole?: SharedInstanceRole | null
|
||||
sharedInstanceSignedOut?: boolean
|
||||
sharedInstanceUpdateAvailable?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
published: []
|
||||
delete: []
|
||||
'review-update': [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const sharedInstanceWrongAccount = computed(() => props.sharedInstanceWrongAccount ?? false)
|
||||
@@ -68,6 +76,14 @@ const showSharedInstancePublishAdmonition = computed(
|
||||
props.instance.shared_instance?.role === 'owner' &&
|
||||
props.instance.shared_instance.status === 'stale',
|
||||
)
|
||||
const showSharedInstanceUpdateAdmonition = computed(
|
||||
() =>
|
||||
!sharedInstanceWrongAccount.value &&
|
||||
!displayedSharedInstanceUnavailableReason.value &&
|
||||
props.instance.install_stage === 'installed' &&
|
||||
props.sharedInstanceRole === 'member' &&
|
||||
props.sharedInstanceUpdateAvailable === true,
|
||||
)
|
||||
|
||||
const stackItems = computed<InstanceAdmonitionItem[]>(() => {
|
||||
const items: InstanceAdmonitionItem[] = []
|
||||
@@ -104,6 +120,15 @@ const stackItems = computed<InstanceAdmonitionItem[]>(() => {
|
||||
})
|
||||
}
|
||||
|
||||
if (showSharedInstanceUpdateAdmonition.value) {
|
||||
items.push({
|
||||
id: 'shared-instance-update-available',
|
||||
type: 'info',
|
||||
dismissible: false,
|
||||
kind: 'shared-instance-update-available',
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
|
||||
+13
@@ -21,6 +21,19 @@ export const instanceAdmonitionsMessages = defineMessages({
|
||||
id: 'app.instance.admonitions.shared-instance.reviewing-button',
|
||||
defaultMessage: 'Reviewing...',
|
||||
},
|
||||
sharedInstanceUpdateAvailableHeader: {
|
||||
id: 'app.instance.admonitions.shared-instance.update-available-header',
|
||||
defaultMessage: 'An update is available',
|
||||
},
|
||||
sharedInstanceUpdateAvailableBody: {
|
||||
id: 'app.instance.admonitions.shared-instance.update-available-body',
|
||||
defaultMessage:
|
||||
'An update is required to play {name}. Please update to latest version to launch the game.',
|
||||
},
|
||||
sharedInstanceReviewUpdateButton: {
|
||||
id: 'app.instance.admonitions.shared-instance.review-update-button',
|
||||
defaultMessage: 'Review update',
|
||||
},
|
||||
sharedInstanceReviewHeader: {
|
||||
id: 'app.instance.admonitions.shared-instance.review-header',
|
||||
defaultMessage: 'Review changes',
|
||||
|
||||
+22
-18
@@ -6,23 +6,27 @@
|
||||
>
|
||||
{{ formatMessage(messages.sharedInstanceChangesBody) }}
|
||||
<template #actions>
|
||||
<ButtonStyled color="blue">
|
||||
<button class="!h-10" :disabled="isPublishButtonDisabled" @click="reviewChanges">
|
||||
<SpinnerIcon
|
||||
v-if="isReviewingPublish || isPublishing"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<UploadIcon v-else aria-hidden="true" />
|
||||
{{
|
||||
isPublishing
|
||||
? formatMessage(messages.sharedInstancePublishingButton)
|
||||
: isReviewingPublish
|
||||
? formatMessage(messages.sharedInstanceReviewingButton)
|
||||
: formatMessage(messages.sharedInstancePublishButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="blue"
|
||||
size="lg"
|
||||
:disabled="isPublishButtonDisabled"
|
||||
@click="reviewChanges"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="isReviewingPublish || isPublishing"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<UploadIcon v-else aria-hidden="true" />
|
||||
{{
|
||||
isPublishing
|
||||
? formatMessage(messages.sharedInstancePublishingButton)
|
||||
: isReviewingPublish
|
||||
? formatMessage(messages.sharedInstanceReviewingButton)
|
||||
: formatMessage(messages.sharedInstancePublishButton)
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
</Admonition>
|
||||
|
||||
@@ -36,7 +40,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SpinnerIcon, UploadIcon } from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, useVIntl } from '@modrinth/ui'
|
||||
import { Admonition, Button, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import SharedInstancePublishModal from '@/components/ui/shared-instances/SharedInstancePublishModal.vue'
|
||||
|
||||
+5
-7
@@ -8,19 +8,17 @@
|
||||
>
|
||||
{{ formatSharedInstanceUnavailable(reason ?? null, manager) }}
|
||||
<template v-if="reason === 'quarantined'" #actions>
|
||||
<ButtonStyled color="orange">
|
||||
<button class="!h-10" @click="emit('delete')">
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.deleteInstance) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="orange" size="lg" @click="emit('delete')">
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.deleteInstance) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Admonition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TrashIcon } from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { Admonition, Button, defineMessages, useVIntl } from '@modrinth/ui'
|
||||
|
||||
import type { SharedInstanceUnavailableReason } from '@/helpers/install'
|
||||
import {
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<Admonition
|
||||
type="info"
|
||||
inline-actions
|
||||
:header="formatMessage(messages.sharedInstanceUpdateAvailableHeader)"
|
||||
>
|
||||
{{ formatMessage(messages.sharedInstanceUpdateAvailableBody, { name: instanceName }) }}
|
||||
<template #actions>
|
||||
<Button type="colored" color="blue" size="lg" @click="emit('review', $event)">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.sharedInstanceReviewUpdateButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</Admonition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon } from '@modrinth/assets'
|
||||
import { Admonition, Button, useVIntl } from '@modrinth/ui'
|
||||
|
||||
import { instanceAdmonitionsMessages as messages } from './instance-admonitions-messages'
|
||||
|
||||
defineProps<{
|
||||
instanceName: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
review: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
</script>
|
||||
@@ -2,6 +2,7 @@ import type { StackedAdmonitionItem } from '@modrinth/ui'
|
||||
|
||||
export type InstanceAdmonitionKind =
|
||||
| 'shared-instance-stale'
|
||||
| 'shared-instance-update-available'
|
||||
| 'shared-instance-unavailable'
|
||||
| 'shared-instance-wrong-account'
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chips,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
StyledInput,
|
||||
TeleportOverflowMenu,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
@@ -109,9 +109,13 @@ watch(selectedReleaseChannel, async (channel, previousChannel) => {
|
||||
})
|
||||
|
||||
async function resetIcon() {
|
||||
icon.value = undefined
|
||||
await edit_icon(instance.value.id, null).catch(handleError)
|
||||
trackEvent('InstanceRemoveIcon')
|
||||
try {
|
||||
await edit_icon(instance.value.id, null)
|
||||
icon.value = undefined
|
||||
trackEvent('InstanceRemoveIcon')
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function setIcon() {
|
||||
@@ -127,10 +131,13 @@ async function setIcon() {
|
||||
|
||||
if (!value) return
|
||||
|
||||
icon.value = value
|
||||
await edit_icon(instance.value.id, icon.value).catch(handleError)
|
||||
|
||||
trackEvent('InstanceSetIcon')
|
||||
try {
|
||||
await edit_icon(instance.value.id, value)
|
||||
icon.value = value
|
||||
trackEvent('InstanceSetIcon')
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const editInstanceObject = computed(() => ({
|
||||
@@ -291,17 +298,24 @@ const messages = defineMessages({
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">Icon</span>
|
||||
<div class="group relative w-fit">
|
||||
<OverflowMenu
|
||||
v-tooltip="formatMessage(messages.editIcon)"
|
||||
class="bg-transparent border-none appearance-none p-0 m-0 cursor-pointer group-active:scale-95 transition-transform"
|
||||
<TeleportOverflowMenu
|
||||
:label="formatMessage(messages.editIcon)"
|
||||
:tooltip="formatMessage(messages.editIcon)"
|
||||
:icon-only="false"
|
||||
type="quiet"
|
||||
interaction="none"
|
||||
class="m-0 !h-auto cursor-pointer appearance-none border-none bg-transparent !p-0 transition-transform group-active:scale-95"
|
||||
:options="[
|
||||
{
|
||||
id: 'select',
|
||||
label: icon
|
||||
? formatMessage(messages.replaceIcon)
|
||||
: formatMessage(messages.selectIcon),
|
||||
action: () => setIcon(),
|
||||
},
|
||||
{
|
||||
id: 'remove',
|
||||
color: 'danger',
|
||||
label: formatMessage(messages.removeIcon),
|
||||
action: () => resetIcon(),
|
||||
shown: !!icon,
|
||||
},
|
||||
@@ -324,7 +338,7 @@ const messages = defineMessages({
|
||||
{{ icon ? formatMessage(messages.replaceIcon) : formatMessage(messages.selectIcon) }}
|
||||
</template>
|
||||
<template #remove> <TrashIcon /> {{ formatMessage(messages.removeIcon) }} </template>
|
||||
</OverflowMenu>
|
||||
</TeleportOverflowMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,17 +359,15 @@ const messages = defineMessages({
|
||||
<h2 id="duplicate-instance-label" class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.duplicateInstance) }}
|
||||
</h2>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||
aria-labelledby="duplicate-instance-label"
|
||||
:disabled="installing"
|
||||
class="w-max !shadow-none"
|
||||
@click="duplicateInstance"
|
||||
>
|
||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
v-tooltip="installing ? formatMessage(messages.duplicateButtonTooltipInstalling) : null"
|
||||
aria-labelledby="duplicate-instance-label"
|
||||
:disabled="installing"
|
||||
class="w-max"
|
||||
@click="duplicateInstance"
|
||||
>
|
||||
<CopyIcon /> {{ formatMessage(messages.duplicateButton) }}
|
||||
</Button>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.duplicateInstanceDescription) }}
|
||||
</p>
|
||||
@@ -381,11 +393,9 @@ const messages = defineMessages({
|
||||
class="w-full max-w-[300px]"
|
||||
@submit="() => addCategory"
|
||||
/>
|
||||
<ButtonStyled>
|
||||
<button class="w-fit !shadow-none" @click="() => addCategory()">
|
||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button class="w-fit" @click="() => addCategory()">
|
||||
<PlusIcon /> {{ formatMessage(messages.libraryGroupsCreate) }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
@@ -414,22 +424,22 @@ const messages = defineMessages({
|
||||
<h2 id="delete-instance-label" class="m-0 text-lg font-semibold text-contrast block">
|
||||
{{ formatMessage(messages.deleteInstance) }}
|
||||
</h2>
|
||||
<ButtonStyled color="red">
|
||||
<button
|
||||
aria-labelledby="delete-instance-label"
|
||||
:disabled="removing"
|
||||
class="w-fit !shadow-none"
|
||||
@click="deleteConfirmModal.show()"
|
||||
>
|
||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||
<TrashIcon v-else />
|
||||
{{
|
||||
removing
|
||||
? formatMessage(messages.deletingInstanceButton)
|
||||
: formatMessage(messages.deleteInstanceButton)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="red"
|
||||
aria-labelledby="delete-instance-label"
|
||||
:disabled="removing"
|
||||
class="w-fit"
|
||||
@click="deleteConfirmModal.show()"
|
||||
>
|
||||
<SpinnerIcon v-if="removing" class="animate-spin" />
|
||||
<TrashIcon v-else />
|
||||
{{
|
||||
removing
|
||||
? formatMessage(messages.deletingInstanceButton)
|
||||
: formatMessage(messages.deleteInstanceButton)
|
||||
}}
|
||||
</Button>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.deleteInstanceDescription) }}
|
||||
</p>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
XCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
Checkbox,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
@@ -224,45 +224,60 @@ const messages = defineMessages({
|
||||
wrapper-class="flex-1 min-w-0"
|
||||
@update:model-value="(val) => (javaPath = String(val))"
|
||||
/>
|
||||
<ButtonStyled
|
||||
<Button
|
||||
type="quiet"
|
||||
:color="
|
||||
!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
: undefined
|
||||
"
|
||||
color-fill="text"
|
||||
:disabled="!overrideJavaInstall || testingJava"
|
||||
:style="{
|
||||
'--legacy-button-color':
|
||||
(!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') &&
|
||||
(!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard') !== 'standard'
|
||||
? `var(--color-${
|
||||
!hoveringTest && !testingJava
|
||||
? javaTestResult === true
|
||||
? 'green'
|
||||
: 'red'
|
||||
: 'standard'
|
||||
})`
|
||||
: undefined,
|
||||
}"
|
||||
class="!text-[var(--legacy-button-color,var(--color-base))] [&>svg]:!text-[var(--legacy-button-color,var(--color-primary))]"
|
||||
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
|
||||
@mouseenter="overrideJavaInstall && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<button
|
||||
:disabled="!overrideJavaInstall || testingJava"
|
||||
@click="testJavaInstallation(activePath, optimalJava?.parsed_version, true)"
|
||||
@mouseenter="overrideJavaInstall && (hoveringTest = true)"
|
||||
@mouseleave="hoveringTest = false"
|
||||
>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="javaTestResult === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon v-else-if="javaTestResult !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="overrideJavaInstall" class="h-4 w-4" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<SpinnerIcon v-if="testingJava" class="animate-spin h-4 w-4" />
|
||||
<CheckCircleIcon
|
||||
v-else-if="javaTestResult === true && !hoveringTest"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<XCircleIcon v-else-if="javaTestResult !== true && !hoveringTest" class="h-4 w-4" />
|
||||
<RefreshCwIcon v-else-if="overrideJavaInstall" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="overrideJavaInstall" class="flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button @click="handleDetectJava">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="handleBrowseJava">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="handleDetectJava">
|
||||
<SearchIcon />
|
||||
Detect
|
||||
</Button>
|
||||
<Button @click="handleBrowseJava">
|
||||
<FolderSearchIcon />
|
||||
Browse
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,192 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-8">
|
||||
<section class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.activeInvitesTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 text-secondary">
|
||||
{{ formatMessage(messages.activeInvitesDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Table :columns="inviteColumns" :data="activeInvites" row-key="id" table-min-width="36rem">
|
||||
<template #empty-state>
|
||||
<div class="flex h-40 items-center justify-center text-secondary">
|
||||
<SpinnerIcon
|
||||
v-if="activeInvitesQuery.isLoading.value"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<template v-else>
|
||||
{{ formatMessage(messages.noActiveInvites) }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell-id="{ row }">
|
||||
<CopyCode
|
||||
:text="`${config.siteUrl}/share/${encodeURIComponent(row.id)}`"
|
||||
:display-text="`/${row.id}`"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-uses="{ row }">
|
||||
<span class="font-medium text-primary">{{ row.uses }}</span>
|
||||
<span> / {{ row.maxUses }}</span>
|
||||
</template>
|
||||
<template #cell-expiration="{ row }">
|
||||
<span v-tooltip="formatDateTime(row.expiration)" class="whitespace-nowrap">
|
||||
{{ formatRelativeTime(row.expiration) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #cell-actions="{ row }">
|
||||
<div class="flex justify-end">
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.revokeInvite)"
|
||||
type="quiet"
|
||||
:label="
|
||||
formatMessage(messages.revokeInviteWithCode, {
|
||||
code: row.id,
|
||||
})
|
||||
"
|
||||
:disabled="revokeInviteMutation.isPending.value || isBusy"
|
||||
class="text-secondary hover:!filter-none hover:text-red focus-visible:!filter-none"
|
||||
@click="revokeInviteModal?.show(row.id)"
|
||||
>
|
||||
<SpinnerIcon
|
||||
v-if="
|
||||
revokeInviteMutation.isPending.value &&
|
||||
revokeInviteMutation.variables.value?.inviteId === row.id
|
||||
"
|
||||
class="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<XIcon v-else aria-hidden="true" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
</Table>
|
||||
</section>
|
||||
|
||||
<SharedInstanceInstallationSettingsControls
|
||||
can-unpublish
|
||||
:busy="isBusy"
|
||||
:unpublishing="unpublishing"
|
||||
:unpublish="unpublishSharedInstance"
|
||||
/>
|
||||
|
||||
<ConfirmRevokeSharedInstanceInviteModal ref="revokeInviteModal" @revoke="revokeInvite" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { IconButton } from '@modrinth/ui'
|
||||
import {
|
||||
CopyCode,
|
||||
defineMessages,
|
||||
Table,
|
||||
type TableColumn,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import ConfirmRevokeSharedInstanceInviteModal from '@/components/ui/shared-instances/ConfirmRevokeSharedInstanceInviteModal.vue'
|
||||
import SharedInstanceInstallationSettingsControls from '@/components/ui/shared-instances/SharedInstanceInstallationSettingsControls.vue'
|
||||
import { unpublish_shared_instance } from '@/helpers/instance'
|
||||
import { config } from '@/config'
|
||||
import {
|
||||
get_shared_instance_invites,
|
||||
revoke_shared_instance_invite,
|
||||
type SharedInstanceInvite,
|
||||
unpublish_shared_instance,
|
||||
} from '@/helpers/instance'
|
||||
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import { injectInstanceSettings } from '@/providers/instance-settings'
|
||||
|
||||
const { instance, offline, onUnlinked } = injectInstanceSettings()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { notifySharedInstanceError } = useSharedInstanceErrors()
|
||||
const { formatMessage } = useVIntl()
|
||||
const queryClient = useQueryClient()
|
||||
const unpublishing = ref(false)
|
||||
const isBusy = computed(
|
||||
() => instance.value.install_stage !== 'installed' || unpublishing.value || !!offline,
|
||||
const revokeInviteModal = ref<InstanceType<typeof ConfirmRevokeSharedInstanceInviteModal>>()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
const formatDateTime = useFormatDateTime({ dateStyle: 'medium', timeStyle: 'short' })
|
||||
|
||||
type InviteTableColumn = 'id' | 'uses' | 'expiration' | 'actions'
|
||||
type ActiveInvitesQueryKey = readonly ['sharedInstanceInvites', string]
|
||||
|
||||
const activeInvitesQueryKey = computed(
|
||||
() => ['sharedInstanceInvites', instance.value.id] as const satisfies ActiveInvitesQueryKey,
|
||||
)
|
||||
const activeInvitesQuery = useQuery({
|
||||
queryKey: activeInvitesQueryKey,
|
||||
queryFn: async ({ queryKey }) => {
|
||||
try {
|
||||
return await get_shared_instance_invites(queryKey[1])
|
||||
} catch (error) {
|
||||
notifySharedInstanceError(error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
enabled: () => !!instance.value.id && !offline,
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: 'always',
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
})
|
||||
const activeInvites = computed(() => activeInvitesQuery.data.value ?? [])
|
||||
|
||||
const revokeInviteMutation = useMutation({
|
||||
mutationFn: ({ instanceId, inviteId }: { instanceId: string; inviteId: string }) =>
|
||||
revoke_shared_instance_invite(instanceId, inviteId),
|
||||
onSuccess: (_data, { instanceId, inviteId }) => {
|
||||
queryClient.setQueryData<SharedInstanceInvite[]>(
|
||||
['sharedInstanceInvites', instanceId],
|
||||
(invites = []) => invites.filter((invite) => invite.id !== inviteId),
|
||||
)
|
||||
},
|
||||
onError: notifySharedInstanceError,
|
||||
})
|
||||
|
||||
const isBusy = computed(
|
||||
() =>
|
||||
instance.value.install_stage !== 'installed' ||
|
||||
unpublishing.value ||
|
||||
revokeInviteMutation.isPending.value ||
|
||||
!!offline,
|
||||
)
|
||||
|
||||
const inviteColumns = computed<TableColumn<InviteTableColumn>[]>(() => [
|
||||
{
|
||||
key: 'id',
|
||||
label: formatMessage(messages.inviteCodeLabel),
|
||||
width: 'clamp(11rem, 34%, 19rem)',
|
||||
},
|
||||
{
|
||||
key: 'uses',
|
||||
label: formatMessage(messages.usesLabel),
|
||||
width: 'clamp(7rem, 18%, 10rem)',
|
||||
},
|
||||
{
|
||||
key: 'expiration',
|
||||
label: formatMessage(messages.expiresLabel),
|
||||
width: 'clamp(9rem, 28%, 14rem)',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: formatMessage(messages.actionsLabel),
|
||||
align: 'right',
|
||||
width: '5.5rem',
|
||||
},
|
||||
])
|
||||
|
||||
function revokeInvite(inviteId: string) {
|
||||
if (revokeInviteMutation.isPending.value) return
|
||||
revokeInviteMutation.mutate({ instanceId: instance.value.id, inviteId })
|
||||
}
|
||||
|
||||
async function unpublishSharedInstance() {
|
||||
unpublishing.value = true
|
||||
@@ -34,9 +196,48 @@ async function unpublishSharedInstance() {
|
||||
await queryClient.invalidateQueries({ queryKey: ['linkedModpackInfo', instance.value.id] })
|
||||
onUnlinked()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
notifySharedInstanceError(error)
|
||||
} finally {
|
||||
unpublishing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
activeInvitesTitle: {
|
||||
id: 'instance.settings.sharing.active-invites.title',
|
||||
defaultMessage: 'Active invites',
|
||||
},
|
||||
activeInvitesDescription: {
|
||||
id: 'instance.settings.sharing.active-invites.description',
|
||||
defaultMessage: 'Anyone with one of these invite links can join while it remains active.',
|
||||
},
|
||||
inviteCodeLabel: {
|
||||
id: 'instance.settings.sharing.active-invites.code',
|
||||
defaultMessage: 'Invite link',
|
||||
},
|
||||
usesLabel: {
|
||||
id: 'instance.settings.sharing.active-invites.uses',
|
||||
defaultMessage: 'Uses',
|
||||
},
|
||||
expiresLabel: {
|
||||
id: 'instance.settings.sharing.active-invites.expires',
|
||||
defaultMessage: 'Expires',
|
||||
},
|
||||
actionsLabel: {
|
||||
id: 'instance.settings.sharing.active-invites.actions',
|
||||
defaultMessage: 'Actions',
|
||||
},
|
||||
noActiveInvites: {
|
||||
id: 'instance.settings.sharing.active-invites.empty',
|
||||
defaultMessage: 'There are no active invites.',
|
||||
},
|
||||
revokeInvite: {
|
||||
id: 'instance.settings.sharing.active-invites.revoke',
|
||||
defaultMessage: 'Revoke invite',
|
||||
},
|
||||
revokeInviteWithCode: {
|
||||
id: 'instance.settings.sharing.active-invites.revoke-with-code',
|
||||
defaultMessage: 'Revoke invite {code}',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
+22
-21
@@ -7,7 +7,7 @@ import {
|
||||
MessagesSquareIcon,
|
||||
WrenchIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, Collapsible, NewModal } from '@modrinth/ui'
|
||||
import { Admonition, Button, ButtonLink, Collapsible, IconButton, NewModal } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
@@ -137,16 +137,18 @@ async function copyToClipboard(text: string) {
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled>
|
||||
<a href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||
<MessagesSquareIcon /> Contact support
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="loadingSignIn" class="!w-full" @click="signInAgain">
|
||||
<LogInIcon /> Sign in again
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonLink href="https://support.modrinth.com" class="!w-full" @click="modal?.hide()">
|
||||
<MessagesSquareIcon /> Contact support
|
||||
</ButtonLink>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="loadingSignIn"
|
||||
class="!w-full"
|
||||
@click="signInAgain"
|
||||
>
|
||||
<LogInIcon /> Sign in again
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -176,16 +178,15 @@ async function copyToClipboard(text: string) {
|
||||
>
|
||||
{{ debugInfo }}
|
||||
</div>
|
||||
<ButtonStyled circular>
|
||||
<button
|
||||
v-tooltip="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<IconButton
|
||||
v-tooltip="'Copy debug info'"
|
||||
:label="'Copy debug info'"
|
||||
:disabled="copied"
|
||||
@click="copyToClipboard(debugInfo)"
|
||||
>
|
||||
<template v-if="copied"> <CheckIcon class="text-green" /> </template>
|
||||
<template v-else> <CopyIcon /> </template>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
+28
-33
@@ -21,38 +21,33 @@
|
||||
|
||||
<div class="flex flex-col gap-6 px-6 pb-6">
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled>
|
||||
<a class="w-full !shadow-none" href="https://support.modrinth.com" @click="modal?.hide()">
|
||||
<MessagesSquareIcon />
|
||||
{{ formatMessage(messages.getSupport) }}
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button class="w-full !shadow-none" :disabled="loadingSignIn" @click="signIn">
|
||||
<SpinnerIcon v-if="loadingSignIn" class="animate-spin" />
|
||||
<svg
|
||||
v-else
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect x="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect y="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect
|
||||
x="10.75"
|
||||
y="10.75"
|
||||
width="9.25"
|
||||
height="9.25"
|
||||
fill="black"
|
||||
fill-opacity="0.9"
|
||||
/>
|
||||
</svg>
|
||||
{{ formatMessage(messages.signIn) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonLink class="w-full" href="https://support.modrinth.com" @click="modal?.hide()">
|
||||
<MessagesSquareIcon />
|
||||
{{ formatMessage(messages.getSupport) }}
|
||||
</ButtonLink>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
class="w-full"
|
||||
:disabled="loadingSignIn"
|
||||
@click="signIn"
|
||||
>
|
||||
<SpinnerIcon v-if="loadingSignIn" class="animate-spin" />
|
||||
<svg
|
||||
v-else
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect x="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect y="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
<rect x="10.75" y="10.75" width="9.25" height="9.25" fill="black" fill-opacity="0.9" />
|
||||
</svg>
|
||||
{{ formatMessage(messages.signIn) }}
|
||||
</Button>
|
||||
</div>
|
||||
<p class="m-0 text-center text-sm text-secondary">
|
||||
{{ formatMessage(messages.dontHaveAccount) }}
|
||||
@@ -69,7 +64,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MessagesSquareIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { Button, ButtonLink, defineMessages, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { inject, type Ref, ref } from 'vue'
|
||||
|
||||
import steveImage from '@/assets/steve-look-up-left.webp'
|
||||
|
||||
@@ -3,12 +3,14 @@ import {
|
||||
CoffeeIcon,
|
||||
GameIcon,
|
||||
GaugeIcon,
|
||||
HeartHandshakeIcon,
|
||||
LanguagesIcon,
|
||||
ModrinthIcon,
|
||||
PaintbrushIcon,
|
||||
SettingsIcon,
|
||||
Settings2Icon,
|
||||
ShieldIcon,
|
||||
ToggleRightIcon,
|
||||
UserIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
commonMessages,
|
||||
@@ -17,23 +19,32 @@ import {
|
||||
defineMessages,
|
||||
ProgressBar,
|
||||
TabbedModal,
|
||||
UnsavedChangesPopup,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, provide, ref, watch } from 'vue'
|
||||
|
||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||
import LanguageSettings from '@/components/ui/settings/LanguageSettings.vue'
|
||||
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
||||
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
||||
import PrivacySettings from '@/components/ui/settings/account/PrivacySettings.vue'
|
||||
import ProfileSettings from '@/components/ui/settings/account/ProfileSettings.vue'
|
||||
import SocialSettings from '@/components/ui/settings/account/SocialSettings.vue'
|
||||
import AppearanceSettings from '@/components/ui/settings/display/AppearanceSettings.vue'
|
||||
import BehaviorSettings from '@/components/ui/settings/display/BehaviorSettings.vue'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/display/FeatureFlagSettings.vue'
|
||||
import LanguageSettings from '@/components/ui/settings/display/LanguageSettings.vue'
|
||||
import DefaultInstanceSettings from '@/components/ui/settings/instances/DefaultInstanceSettings.vue'
|
||||
import JavaSettings from '@/components/ui/settings/instances/JavaSettings.vue'
|
||||
import ResourceManagementSettings from '@/components/ui/settings/instances/ResourceManagementSettings.vue'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import {
|
||||
appSettingsModalContextKey,
|
||||
type UnsavedChangesController,
|
||||
} from '@/providers/app-settings-modal'
|
||||
import { injectAppUpdateDownloadProgress } from '@/providers/download-progress.ts'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
// TODO: Apply COMPONENT_STRUCTURE.md here and extract out common setting option components
|
||||
const themeStore = useTheming()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -45,71 +56,162 @@ const developerModeEnabled = defineMessage({
|
||||
defaultMessage: 'Developer mode enabled.',
|
||||
})
|
||||
|
||||
const tabCategories = defineMessages({
|
||||
display: {
|
||||
id: 'settings.sidebar.label.display',
|
||||
defaultMessage: 'Display',
|
||||
},
|
||||
account: {
|
||||
id: 'settings.sidebar.label.account',
|
||||
defaultMessage: 'Account',
|
||||
},
|
||||
instances: {
|
||||
id: 'app.settings.sidebar.label.instances',
|
||||
defaultMessage: 'Instances',
|
||||
},
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.appearance',
|
||||
defaultMessage: 'Appearance',
|
||||
}),
|
||||
category: tabCategories.display,
|
||||
icon: PaintbrushIcon,
|
||||
content: AppearanceSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.behavior',
|
||||
defaultMessage: 'Behavior',
|
||||
}),
|
||||
category: tabCategories.display,
|
||||
icon: Settings2Icon,
|
||||
content: BehaviorSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.language',
|
||||
defaultMessage: 'Language',
|
||||
}),
|
||||
category: tabCategories.display,
|
||||
icon: LanguagesIcon,
|
||||
content: LanguageSettings,
|
||||
badge: commonMessages.beta,
|
||||
},
|
||||
{
|
||||
name: commonSettingsMessages.featureFlags,
|
||||
category: tabCategories.display,
|
||||
icon: ToggleRightIcon,
|
||||
content: FeatureFlagSettings,
|
||||
developerOnly: true,
|
||||
},
|
||||
{
|
||||
name: commonSettingsMessages.profile,
|
||||
category: tabCategories.account,
|
||||
icon: UserIcon,
|
||||
content: ProfileSettings,
|
||||
},
|
||||
{
|
||||
name: commonSettingsMessages.social,
|
||||
category: tabCategories.account,
|
||||
icon: HeartHandshakeIcon,
|
||||
content: SocialSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.privacy',
|
||||
defaultMessage: 'Privacy',
|
||||
}),
|
||||
category: tabCategories.account,
|
||||
icon: ShieldIcon,
|
||||
content: PrivacySettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.default-instance-options',
|
||||
defaultMessage: 'Default game options',
|
||||
}),
|
||||
category: tabCategories.instances,
|
||||
icon: GameIcon,
|
||||
content: DefaultInstanceSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.java-installations',
|
||||
defaultMessage: 'Java installations',
|
||||
}),
|
||||
category: tabCategories.instances,
|
||||
icon: CoffeeIcon,
|
||||
content: JavaSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.default-instance-options',
|
||||
defaultMessage: 'Default instance options',
|
||||
}),
|
||||
icon: GameIcon,
|
||||
content: DefaultInstanceSettings,
|
||||
},
|
||||
{
|
||||
name: defineMessage({
|
||||
id: 'app.settings.tabs.resource-management',
|
||||
defaultMessage: 'Resource management',
|
||||
}),
|
||||
category: tabCategories.instances,
|
||||
icon: GaugeIcon,
|
||||
content: ResourceManagementSettings,
|
||||
},
|
||||
{
|
||||
name: commonSettingsMessages.featureFlags,
|
||||
icon: ToggleRightIcon,
|
||||
content: FeatureFlagSettings,
|
||||
developerOnly: true,
|
||||
},
|
||||
]
|
||||
|
||||
const availableTabs = computed(() => tabs.filter((tab) => !tab.developerOnly || themeStore.devMode))
|
||||
|
||||
const modal = ref<InstanceType<typeof TabbedModal> | null>(null)
|
||||
const unsavedChangesPopup = ref<{ nudge: () => void } | null>(null)
|
||||
const unsavedChangesController = ref<UnsavedChangesController | null>(null)
|
||||
const emptyUnsavedChangesState: Record<string, unknown> = {}
|
||||
const originalUnsavedChangesState = computed(
|
||||
() => unsavedChangesController.value?.getOriginal() ?? emptyUnsavedChangesState,
|
||||
)
|
||||
const modifiedUnsavedChangesState = computed(
|
||||
() => unsavedChangesController.value?.getModified() ?? emptyUnsavedChangesState,
|
||||
)
|
||||
const savingUnsavedChanges = computed(() => unsavedChangesController.value?.isSaving() ?? false)
|
||||
const hasUnsavedChanges = computed(() => unsavedChangesController.value?.hasChanges() ?? false)
|
||||
|
||||
function canLeaveCurrentTab(): boolean {
|
||||
if (!unsavedChangesController.value?.hasChanges()) return true
|
||||
unsavedChangesPopup.value?.nudge()
|
||||
return false
|
||||
}
|
||||
|
||||
function close(): boolean {
|
||||
return modal.value?.hide() ?? false
|
||||
}
|
||||
|
||||
function registerUnsavedChangesController(controller: UnsavedChangesController | null): void {
|
||||
unsavedChangesController.value = controller
|
||||
}
|
||||
|
||||
provide(appSettingsModalContextKey, {
|
||||
close,
|
||||
registerUnsavedChangesController,
|
||||
})
|
||||
|
||||
function resetUnsavedChanges(): void {
|
||||
unsavedChangesController.value?.reset()
|
||||
}
|
||||
|
||||
function saveUnsavedChanges(): void {
|
||||
void unsavedChangesController.value?.save()
|
||||
}
|
||||
|
||||
function show() {
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({ show })
|
||||
function showProfile(): void {
|
||||
const profileTabIndex = availableTabs.value.findIndex((tab) => tab.content === ProfileSettings)
|
||||
if (profileTabIndex >= 0) {
|
||||
modal.value?.setTab(profileTabIndex)
|
||||
}
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
defineExpose({ show, showProfile })
|
||||
|
||||
const { progress, version: downloadingVersion } = injectAppUpdateDownloadProgress()
|
||||
|
||||
@@ -129,12 +231,15 @@ watch(
|
||||
function devModeCount() {
|
||||
devModeCounter.value++
|
||||
if (devModeCounter.value > 5) {
|
||||
const selectedTab = modal.value ? availableTabs.value[modal.value.selectedTab] : undefined
|
||||
|
||||
themeStore.devMode = !themeStore.devMode
|
||||
settings.value.developer_mode = !!themeStore.devMode
|
||||
devModeCounter.value = 0
|
||||
|
||||
if (!themeStore.devMode && tabs[modal.value!.selectedTab].developerOnly) {
|
||||
modal.value!.setTab(0)
|
||||
if (modal.value) {
|
||||
const selectedTabIndex = selectedTab ? availableTabs.value.indexOf(selectedTab) : -1
|
||||
modal.value.setTab(selectedTabIndex >= 0 ? selectedTabIndex : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,15 +249,45 @@ const messages = defineMessages({
|
||||
id: 'app.settings.downloading',
|
||||
defaultMessage: 'Downloading v{version}',
|
||||
},
|
||||
appVersion: {
|
||||
id: 'app.settings.app-version',
|
||||
defaultMessage: 'Modrinth App {version}',
|
||||
},
|
||||
macos: {
|
||||
id: 'app.settings.operating-system.macos',
|
||||
defaultMessage: 'macOS',
|
||||
},
|
||||
developerModeButtonLabel: {
|
||||
id: 'app.settings.developer-mode-button.label',
|
||||
defaultMessage: 'Toggle developer mode',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<TabbedModal ref="modal" :tabs="tabs.filter((t) => !t.developerOnly || themeStore.devMode)">
|
||||
<TabbedModal
|
||||
ref="modal"
|
||||
:tabs="availableTabs"
|
||||
:width="'min(928px, calc(95vw - 10rem))'"
|
||||
:before-hide="canLeaveCurrentTab"
|
||||
:before-tab-change="canLeaveCurrentTab"
|
||||
:floating-action-bar-shown="hasUnsavedChanges"
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex items-center gap-2 text-lg font-extrabold text-contrast">
|
||||
<SettingsIcon /> Settings
|
||||
<span class="text-2xl font-semibold text-contrast">
|
||||
{{ formatMessage(commonMessages.settingsLabel) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #floating-action-bar>
|
||||
<UnsavedChangesPopup
|
||||
ref="unsavedChangesPopup"
|
||||
:original="originalUnsavedChangesState"
|
||||
:modified="modifiedUnsavedChangesState"
|
||||
:saving="savingUnsavedChanges"
|
||||
inline
|
||||
@reset="resetUnsavedChanges"
|
||||
@save="saveUnsavedChanges"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="mt-auto text-secondary text-sm">
|
||||
<div class="mb-3">
|
||||
@@ -168,6 +303,7 @@ const messages = defineMessages({
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.developerModeButtonLabel)"
|
||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||
:class="{
|
||||
'text-brand': themeStore.devMode,
|
||||
@@ -175,12 +311,14 @@ const messages = defineMessages({
|
||||
}"
|
||||
@click="devModeCount"
|
||||
>
|
||||
<ModrinthIcon class="w-6 h-6" />
|
||||
<ModrinthIcon aria-hidden="true" class="w-6 h-6" />
|
||||
</button>
|
||||
<div class="max-w-[200px]">
|
||||
<p class="m-0">Modrinth App {{ version }}</p>
|
||||
<p class="m-0">
|
||||
<span v-if="osPlatform === 'macos'">macOS</span>
|
||||
{{ formatMessage(messages.appVersion, { version }) }}
|
||||
</p>
|
||||
<p class="m-0">
|
||||
<span v-if="osPlatform === 'macos'">{{ formatMessage(messages.macos) }}</span>
|
||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||
{{ osVersion }}
|
||||
</p>
|
||||
|
||||
@@ -6,18 +6,14 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button @click="confirm">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.deleteButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="red" @click="confirm">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(messages.deleteButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -27,7 +23,7 @@
|
||||
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
|
||||
@@ -18,12 +18,10 @@
|
||||
{{ formatMessage(messages.sharedInstance) }}
|
||||
</span>
|
||||
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="openViewContents">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" @click="openViewContents">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 rounded-2xl bg-surface-2 p-3">
|
||||
@@ -113,40 +111,30 @@
|
||||
</p>
|
||||
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<ButtonStyled type="transparent" color="red">
|
||||
<button @click="handleReport">
|
||||
<ReportIcon />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="red" @click="handleReport">
|
||||
<ReportIcon />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</Button>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="hasExternalFiles">
|
||||
<ButtonStyled type="transparent" color="orange">
|
||||
<button @click="handleAccept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleDecline">
|
||||
<BanIcon />
|
||||
{{ formatMessage(messages.dontInstall) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="orange" @click="handleAccept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="handleDecline">
|
||||
<BanIcon />
|
||||
{{ formatMessage(messages.dontInstall) }}
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled>
|
||||
<button @click="handleDecline">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleAccept">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="handleDecline">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="handleAccept">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,10 +151,10 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { BanIcon, DownloadIcon, EyeIcon, ReportIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
type ContentItem,
|
||||
defineMessages,
|
||||
|
||||
@@ -10,24 +10,18 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="handleCreateAnyway">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.create) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</Button>
|
||||
<Button type="colored" color="orange" @click="handleCreateAnyway">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.create) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -36,7 +30,7 @@
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
|
||||
@@ -19,18 +19,20 @@
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<ButtonStyled>
|
||||
<button class="w-full !shadow-none" type="button" @click="authenticate('sign-up')">
|
||||
<UserPlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createAccountButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button class="w-full" type="button" @click="authenticate('sign-in')">
|
||||
<LogInIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.signInButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button class="w-full" native-type="button" @click="authenticate('sign-up')">
|
||||
<UserPlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createAccountButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
class="w-full"
|
||||
native-type="button"
|
||||
@click="authenticate('sign-in')"
|
||||
>
|
||||
<LogInIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.signInButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-center text-base font-medium leading-6 text-primary">
|
||||
@@ -69,23 +71,19 @@
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="w-full" type="button" @click="modal?.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button
|
||||
class="w-full !shadow-none"
|
||||
type="button"
|
||||
:disabled="reopeningBrowser"
|
||||
@click="reopenBrowser"
|
||||
>
|
||||
<RefreshCwIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.openBrowserAgainButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" class="w-full" native-type="button" @click="modal?.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
native-type="button"
|
||||
:disabled="reopeningBrowser"
|
||||
@click="reopenBrowser"
|
||||
>
|
||||
<RefreshCwIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.openBrowserAgainButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-center text-base font-medium leading-6 text-primary">
|
||||
@@ -108,7 +106,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LogInIcon, RefreshCwIcon, SpinnerIcon, UserPlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, defineMessages, IntlFormatted, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { Button, defineMessages, IntlFormatted, NewModal, useVIntl } from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { ref } from 'vue'
|
||||
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Combobox, defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
|
||||
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
||||
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
||||
|
||||
const messages = defineMessages({
|
||||
colorThemeTitle: {
|
||||
id: 'app.appearance-settings.color-theme.title',
|
||||
defaultMessage: 'Color theme',
|
||||
},
|
||||
colorThemeDescription: {
|
||||
id: 'app.appearance-settings.color-theme.description',
|
||||
defaultMessage: 'Select your preferred color theme for Modrinth App.',
|
||||
},
|
||||
advancedRenderingTitle: {
|
||||
id: 'app.appearance-settings.advanced-rendering.title',
|
||||
defaultMessage: 'Advanced rendering',
|
||||
},
|
||||
advancedRenderingDescription: {
|
||||
id: 'app.appearance-settings.advanced-rendering.description',
|
||||
defaultMessage:
|
||||
'Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering.',
|
||||
},
|
||||
hideNametagTitle: {
|
||||
id: 'app.appearance-settings.hide-nametag.title',
|
||||
defaultMessage: 'Hide nametag',
|
||||
},
|
||||
hideNametagDescription: {
|
||||
id: 'app.appearance-settings.hide-nametag.description',
|
||||
defaultMessage: 'Disables the nametag above your player on the skins page.',
|
||||
},
|
||||
nativeDecorationsTitle: {
|
||||
id: 'app.appearance-settings.native-decorations.title',
|
||||
defaultMessage: 'Native decorations',
|
||||
},
|
||||
nativeDecorationsDescription: {
|
||||
id: 'app.appearance-settings.native-decorations.description',
|
||||
defaultMessage: 'Use system window frame (app restart required).',
|
||||
},
|
||||
minimizeLauncherTitle: {
|
||||
id: 'app.appearance-settings.minimize-launcher.title',
|
||||
defaultMessage: 'Minimize launcher',
|
||||
},
|
||||
minimizeLauncherDescription: {
|
||||
id: 'app.appearance-settings.minimize-launcher.description',
|
||||
defaultMessage: 'Minimize the launcher when a Minecraft process starts.',
|
||||
},
|
||||
defaultLandingPageTitle: {
|
||||
id: 'app.appearance-settings.default-landing-page.title',
|
||||
defaultMessage: 'Default landing page',
|
||||
},
|
||||
defaultLandingPageDescription: {
|
||||
id: 'app.appearance-settings.default-landing-page.description',
|
||||
defaultMessage: 'Change the page to which the launcher opens on.',
|
||||
},
|
||||
defaultLandingPageHome: {
|
||||
id: 'app.appearance-settings.default-landing-page.home',
|
||||
defaultMessage: 'Home',
|
||||
},
|
||||
defaultLandingPageLibrary: {
|
||||
id: 'app.appearance-settings.default-landing-page.library',
|
||||
defaultMessage: 'Library',
|
||||
},
|
||||
selectOption: {
|
||||
id: 'app.appearance-settings.select-option',
|
||||
defaultMessage: 'Select an option',
|
||||
},
|
||||
jumpBackIntoWorldsTitle: {
|
||||
id: 'app.appearance-settings.jump-back-into-worlds.title',
|
||||
defaultMessage: 'Jump back into worlds',
|
||||
},
|
||||
jumpBackIntoWorldsDescription: {
|
||||
id: 'app.appearance-settings.jump-back-into-worlds.description',
|
||||
defaultMessage: 'Includes recent worlds in the "Jump back in" section on the Home page.',
|
||||
},
|
||||
toggleSidebarTitle: {
|
||||
id: 'app.appearance-settings.toggle-sidebar.title',
|
||||
defaultMessage: 'Toggle sidebar',
|
||||
},
|
||||
toggleSidebarDescription: {
|
||||
id: 'app.appearance-settings.toggle-sidebar.description',
|
||||
defaultMessage: 'Enables the ability to toggle the sidebar.',
|
||||
},
|
||||
unknownPackWarningTitle: {
|
||||
id: 'app.appearance-settings.unknown-pack-warning.title',
|
||||
defaultMessage: 'Warn me before installing unknown modpacks',
|
||||
},
|
||||
unknownPackWarningDescription: {
|
||||
id: 'app.appearance-settings.unknown-pack-warning.description',
|
||||
defaultMessage:
|
||||
"If you attempt to install a Modrinth Pack file (.mrpack) that isn't hosted on Modrinth, we'll make sure you understand the risks before installing it.",
|
||||
},
|
||||
skipNonEssentialWarningsTitle: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.title',
|
||||
defaultMessage: 'Skip non-essential warnings',
|
||||
},
|
||||
skipNonEssentialWarningsDescription: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.description',
|
||||
defaultMessage:
|
||||
'Automatically skips low-risk confirmations like duplicate modpack installs, normal content deletion, bulk updates, unlinking modpacks, and repair prompts. Dangerous warnings will still be shown.',
|
||||
},
|
||||
showPlayTimeTitle: {
|
||||
id: 'app.appearance-settings.show-play-time.title',
|
||||
defaultMessage: 'Show play time',
|
||||
},
|
||||
showPlayTimeDescription: {
|
||||
id: 'app.appearance-settings.show-play-time.description',
|
||||
defaultMessage: `Displays how much time you've spent playing an instance.`,
|
||||
},
|
||||
})
|
||||
|
||||
const os = ref(await getOS())
|
||||
const settings = ref(await get())
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.colorThemeTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
(theme: ColorTheme) => {
|
||||
themeStore.setThemeState(theme)
|
||||
settings.theme = theme
|
||||
}
|
||||
"
|
||||
:current-theme="settings.theme"
|
||||
:theme-options="themeStore.getThemeOptions()"
|
||||
system-theme-color="system"
|
||||
/>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.advancedRenderingTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.advancedRenderingDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.advancedRendering"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.advancedRendering = !!e
|
||||
settings.advanced_rendering = themeStore.advancedRendering
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.nativeDecorationsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.minimizeLauncherTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.minimizeLauncherDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.showPlayTimeTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
|
||||
themeStore.featureFlags[showPlayTimeFlag] = newValue
|
||||
settings.feature_flags[showPlayTimeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.hideNametagTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="hide-nametag-skins-page"
|
||||
:model-value="themeStore.hideNametagSkinsPage"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.hideNametagSkinsPage = !!e
|
||||
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.defaultLandingPageTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.defaultLandingPageDescription) }}</p>
|
||||
</div>
|
||||
<Combobox
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="max-w-40"
|
||||
:options="[
|
||||
{
|
||||
value: 'Home',
|
||||
label: formatMessage(messages.defaultLandingPageHome),
|
||||
},
|
||||
{
|
||||
value: 'Library',
|
||||
label: formatMessage(messages.defaultLandingPageLibrary),
|
||||
},
|
||||
]"
|
||||
:display-value="settings.default_page ?? 'Select an option'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
|
||||
themeStore.featureFlags[worldsInHomeFlag] = newValue
|
||||
settings.feature_flags[worldsInHomeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.unknownPackWarningTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.unknownPackWarningDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
const warnBeforeUnknownPackInstall = !!e
|
||||
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
|
||||
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
|
||||
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
|
||||
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
|
||||
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.toggleSidebarTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="toggle-sidebar"
|
||||
:model-value="settings.toggle_sidebar"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.toggle_sidebar = !!e
|
||||
themeStore.toggleSidebar = settings.toggle_sidebar
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,186 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { injectNotificationManager, Slider, StyledInput, Toggle } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const fetchSettings = await get()
|
||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
|
||||
|
||||
const settings = ref(fetchSettings)
|
||||
|
||||
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||
maxMemory: number
|
||||
snapPoints: number[]
|
||||
}
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||
|
||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||
setSettings.custom_env_vars = setSettings.envVars
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
|
||||
if (!setSettings.hooks.pre_launch) {
|
||||
setSettings.hooks.pre_launch = null
|
||||
}
|
||||
if (!setSettings.hooks.wrapper) {
|
||||
setSettings.hooks.wrapper = null
|
||||
}
|
||||
if (!setSettings.hooks.post_exit) {
|
||||
setSettings.hooks.post_exit = null
|
||||
}
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Fullscreen</h3>
|
||||
<p class="m-0 leading-tight">
|
||||
Overwrites the options.txt file to start in full screen when launched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Width</h3>
|
||||
<p class="m-0 leading-tight">The width of the game window when launched.</p>
|
||||
</div>
|
||||
|
||||
<StyledInput
|
||||
id="width"
|
||||
v-model="settings.game_resolution[0]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter width..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Height</h3>
|
||||
<p class="m-0 leading-tight">The height of the game window when launched.</p>
|
||||
</div>
|
||||
|
||||
<StyledInput
|
||||
id="height"
|
||||
v-model="settings.game_resolution[1]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
placeholder="Enter height..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-6 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Memory allocated</h2>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<p class="m-0 mt-1 leading-tight">The memory allocated to each instance when it is ran.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Java arguments</h2>
|
||||
<StyledInput
|
||||
id="java-args"
|
||||
v-model="settings.launchArgs"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter java arguments..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Environmental variables</h2>
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="settings.envVars"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter environmental variables..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-6 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Pre launch hook</h3>
|
||||
<StyledInput
|
||||
id="pre-launch"
|
||||
v-model="settings.hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter pre-launch command..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">Ran before the instance is launched.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Wrapper hook</h3>
|
||||
<StyledInput
|
||||
id="wrapper"
|
||||
v-model="settings.hooks.wrapper"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter wrapper command..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">Wrapper command for launching Minecraft.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">Post exit hook</h3>
|
||||
<StyledInput
|
||||
id="post-exit"
|
||||
v-model="settings.hooks.post_exit"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
placeholder="Enter post-exit command..."
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">Ran after the game closes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,168 +0,0 @@
|
||||
<script setup>
|
||||
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, Slider, StyledInput } from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const themeStore = useTheming()
|
||||
const settings = ref(await get())
|
||||
const purgeCacheConfirmModal = ref(null)
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
async function purgeCache() {
|
||||
await purge_cache_types([
|
||||
'project',
|
||||
'project_v3',
|
||||
'version',
|
||||
'user',
|
||||
'team',
|
||||
'organization',
|
||||
'file',
|
||||
'loader_manifest',
|
||||
'minecraft_manifest',
|
||||
'categories',
|
||||
'report_types',
|
||||
'loaders',
|
||||
'game_versions',
|
||||
'donation_platforms',
|
||||
'file_hash',
|
||||
'file_update',
|
||||
'search_results',
|
||||
'search_results_v3',
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
function handlePurgeCacheClick() {
|
||||
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
void purgeCache()
|
||||
return
|
||||
}
|
||||
|
||||
purgeCacheConfirmModal.value?.show()
|
||||
}
|
||||
|
||||
async function openDbBackupsFolder() {
|
||||
await showAppDbBackupsFolder().catch(handleError)
|
||||
}
|
||||
|
||||
async function findLauncherDir() {
|
||||
const newDir = await open({
|
||||
multiple: false,
|
||||
directory: true,
|
||||
title: 'Select a new app directory',
|
||||
})
|
||||
|
||||
if (newDir) {
|
||||
settings.value.custom_dir = newDir
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">App directory</h2>
|
||||
<StyledInput
|
||||
id="appDir"
|
||||
v-model="settings.custom_dir"
|
||||
:icon="BoxIcon"
|
||||
type="text"
|
||||
wrapper-class="w-full"
|
||||
>
|
||||
<template #right>
|
||||
<ButtonStyled circular>
|
||||
<button class="ml-1.5" @click="findLauncherDir">
|
||||
<FolderSearchIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</StyledInput>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The directory where the launcher stores all of its files. Changes will be applied after
|
||||
restarting the launcher.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<ConfirmModalWrapper
|
||||
ref="purgeCacheConfirmModal"
|
||||
title="Are you sure you want to purge the cache?"
|
||||
description="If you proceed, your entire cache will be purged. This may slow down the app temporarily."
|
||||
:has-to-type="false"
|
||||
proceed-label="Purge cache"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">App cache</h2>
|
||||
<button id="purge-cache" class="btn min-w-max" @click="handlePurgeCacheClick">
|
||||
<TrashIcon />
|
||||
Purge cache
|
||||
</button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The Modrinth app stores a cache of data to speed up loading. This can be purged to force the
|
||||
app to reload data. This may slow down the app temporarily.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast mt-4">Maximum concurrent downloads</h2>
|
||||
<Slider
|
||||
id="max-downloads"
|
||||
v-model="settings.max_concurrent_downloads"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
/>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can download at the same time. Set this to a lower
|
||||
value if you have a poor internet connection. (app restart required to take effect)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">Maximum concurrent writes</h2>
|
||||
<Slider
|
||||
id="max-writes"
|
||||
v-model="settings.max_concurrent_writes"
|
||||
:min="1"
|
||||
:max="50"
|
||||
:step="1"
|
||||
/>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
The maximum amount of files the launcher can write to the disk at once. Set this to a lower
|
||||
value if you are frequently getting I/O errors. (app restart required to take effect)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">App database backups</h2>
|
||||
<button id="open-db-backups-folder" class="btn min-w-max" @click="openDbBackupsFolder">
|
||||
<FolderOpenIcon />
|
||||
Open backups folder
|
||||
</button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
Backups of important app data are stored here in case you need to recover them later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+36
-25
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Settings2Icon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
injectPageContext,
|
||||
@@ -33,6 +33,24 @@ const messages = defineMessages({
|
||||
id: 'app.ads-consent.manage',
|
||||
defaultMessage: 'Manage preferences',
|
||||
},
|
||||
telemetryTitle: {
|
||||
id: 'app.settings.privacy.telemetry.title',
|
||||
defaultMessage: 'Telemetry',
|
||||
},
|
||||
telemetryDescription: {
|
||||
id: 'app.settings.privacy.telemetry.description',
|
||||
defaultMessage:
|
||||
'Modrinth collects anonymized analytics and usage data to improve our user experience and customize your experience. By disabling this option, you opt out and your data will no longer be collected.',
|
||||
},
|
||||
discordRichPresenceTitle: {
|
||||
id: 'app.settings.privacy.discord-rich-presence.title',
|
||||
defaultMessage: 'Discord Rich Presence',
|
||||
},
|
||||
discordRichPresenceDescription: {
|
||||
id: 'app.settings.privacy.discord-rich-presence.description',
|
||||
defaultMessage:
|
||||
'Show Modrinth App as your current activity on Discord. This does not affect Rich Presence added to instances by mods. Requires an app restart.',
|
||||
},
|
||||
})
|
||||
|
||||
async function manageAdsPreferences() {
|
||||
@@ -59,28 +77,24 @@ watch(
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.adsConsentTitle) }}
|
||||
</h2>
|
||||
<div class="mt-1 flex flex-col gap-2.5 items-start">
|
||||
<div class="flex flex-col gap-1 items-start">
|
||||
<div class="text-sm">
|
||||
{{ formatMessage(messages.adsConsentIntro) }}
|
||||
</div>
|
||||
<div class="mt-2 flex flex-col gap-2.5 items-start">
|
||||
<Button @click="manageAdsPreferences">
|
||||
<Settings2Icon aria-hidden="true" />
|
||||
{{ formatMessage(messages.adsConsentManage) }}
|
||||
</Button>
|
||||
<div>
|
||||
{{ formatMessage(messages.adsConsentIntro) }}
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" @click="manageAdsPreferences">
|
||||
<Settings2Icon aria-hidden="true" />
|
||||
{{ formatMessage(messages.adsConsentManage) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Telemetry</h2>
|
||||
<p class="m-0 mt-1 text-sm">
|
||||
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||
customize your experience. By disabling this option, you opt out and your data will no
|
||||
longer be collected.
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.telemetryTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.telemetryDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle id="opt-out-analytics" v-model="settings.telemetry" />
|
||||
@@ -88,14 +102,11 @@ watch(
|
||||
|
||||
<div class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Discord RPC</h2>
|
||||
<p class="m-0 mt-1 text-sm">
|
||||
Manages the Discord Rich Presence integration. Disabling this will cause 'Modrinth' to no
|
||||
longer show up as a game or app you are using on your Discord profile.
|
||||
</p>
|
||||
<p class="m-0 mt-2 text-sm">
|
||||
Note: This will not prevent any instance-specific Discord Rich Presence integrations, such
|
||||
as those added by mods. (app restart required to take effect)
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.discordRichPresenceTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.discordRichPresenceDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle id="disable-discord-rpc" v-model="settings.discord_rpc" />
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<AccountProfileSettings
|
||||
ref="profileSettings"
|
||||
:patch-user="patchUser"
|
||||
:change-avatar="changeAvatar"
|
||||
:delete-avatar="deleteAvatar"
|
||||
:get-authenticated-user="getAuthenticatedUser"
|
||||
@profile-link-click="handleProfileLinkClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { AccountProfileSettings, injectAuth } from '@modrinth/ui'
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
change_user_avatar,
|
||||
delete_user_avatar,
|
||||
get_user_profile,
|
||||
patch_user,
|
||||
} from '@/helpers/users'
|
||||
import { appSettingsModalContextKey } from '@/providers/app-settings-modal'
|
||||
|
||||
const settingsModal = inject(appSettingsModalContextKey, null)
|
||||
const auth = injectAuth()
|
||||
const profileSettings = ref<InstanceType<typeof AccountProfileSettings> | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
settingsModal?.registerUnsavedChangesController({
|
||||
hasChanges: () => profileSettings.value?.hasChanges ?? false,
|
||||
getOriginal: () => profileSettings.value?.originalState ?? {},
|
||||
getModified: () => profileSettings.value?.modifiedState ?? {},
|
||||
isSaving: () => profileSettings.value?.saving ?? false,
|
||||
reset: () => profileSettings.value?.reset(),
|
||||
save: () => profileSettings.value?.save(),
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
settingsModal?.registerUnsavedChangesController(null)
|
||||
})
|
||||
|
||||
function handleProfileLinkClick(event: MouseEvent): void {
|
||||
if (settingsModal && !settingsModal.close()) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function patchUser(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v2.User, 'bio' | 'username'>>,
|
||||
): Promise<void> {
|
||||
return patch_user(userId, patch)
|
||||
}
|
||||
|
||||
async function changeAvatar(userId: string, file: Blob, extension: string): Promise<void> {
|
||||
await change_user_avatar(userId, new Uint8Array(await file.arrayBuffer()), extension)
|
||||
}
|
||||
|
||||
function deleteAvatar(userId: string): Promise<void> {
|
||||
return delete_user_avatar(userId)
|
||||
}
|
||||
|
||||
function getAuthenticatedUser(): Promise<Labrinth.Users.v3.User> {
|
||||
const userId = auth.user.value?.id
|
||||
if (!userId) throw new Error('Cannot refresh a signed-out user.')
|
||||
return get_user_profile(userId)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<AccountSocialSettings
|
||||
:get-blocked-users="get_blocked_users"
|
||||
:get-users="getUsers"
|
||||
:unblock-user="unblock_user"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { AccountSocialSettings, injectModrinthClient } from '@modrinth/ui'
|
||||
|
||||
import { get_blocked_users, unblock_user } from '@/helpers/users'
|
||||
|
||||
const client = injectModrinthClient()
|
||||
|
||||
function getUsers(userIds: string[]): Promise<Labrinth.Users.v2.User[]> {
|
||||
return client.labrinth.users_v2.getMultiple(userIds)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessages, ThemeSelector, Toggle, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { ColorTheme } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
colorThemeTitle: {
|
||||
id: 'app.appearance-settings.color-theme.title',
|
||||
defaultMessage: 'Color theme',
|
||||
},
|
||||
colorThemeDescription: {
|
||||
id: 'app.appearance-settings.color-theme.description',
|
||||
defaultMessage: 'Choose the color theme used by Modrinth App.',
|
||||
},
|
||||
advancedRenderingTitle: {
|
||||
id: 'app.appearance-settings.advanced-rendering.title',
|
||||
defaultMessage: 'Advanced rendering',
|
||||
},
|
||||
advancedRenderingDescription: {
|
||||
id: 'app.appearance-settings.advanced-rendering.description',
|
||||
defaultMessage:
|
||||
'Enable visual effects such as background blur. This may reduce performance without hardware acceleration.',
|
||||
},
|
||||
nativeDecorationsTitle: {
|
||||
id: 'app.appearance-settings.native-decorations.title',
|
||||
defaultMessage: 'System window frame',
|
||||
},
|
||||
nativeDecorationsDescription: {
|
||||
id: 'app.appearance-settings.native-decorations.description',
|
||||
defaultMessage:
|
||||
"Use your operating system's title bar and window controls. Requires an app restart.",
|
||||
},
|
||||
})
|
||||
|
||||
const os = ref(await getOS())
|
||||
const settings = ref(await get())
|
||||
const themeOptions = computed(() =>
|
||||
themeStore
|
||||
.getThemeOptions()
|
||||
.filter((theme) => theme !== 'retro' || themeStore.devMode || settings.value.theme === 'retro'),
|
||||
)
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.colorThemeTitle) }}
|
||||
</h2>
|
||||
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.colorThemeDescription) }}</p>
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
(theme: ColorTheme) => {
|
||||
themeStore.setThemeState(theme)
|
||||
settings.theme = theme
|
||||
}
|
||||
"
|
||||
:current-theme="settings.theme"
|
||||
:theme-options="themeOptions"
|
||||
system-theme-color="system"
|
||||
/>
|
||||
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.advancedRenderingTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.advancedRenderingDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.advancedRendering"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.advancedRendering = !!e
|
||||
settings.advanced_rendering = themeStore.advancedRendering
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-6 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.nativeDecorationsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.nativeDecorationsDescription) }}</p>
|
||||
</div>
|
||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,302 @@
|
||||
<script setup lang="ts">
|
||||
import { Chips, defineMessages, Toggle, useVIntl } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const worldsInHomeFlag: FeatureFlag = 'worlds_in_home'
|
||||
const skipNonEssentialWarningsFlag: FeatureFlag = 'skip_non_essential_warnings'
|
||||
const skipUnknownPackWarningFlag: FeatureFlag = 'skip_unknown_pack_warning'
|
||||
const showPlayTimeFlag: FeatureFlag = 'show_instance_play_time'
|
||||
|
||||
type LandingPage = 'Home' | 'Library'
|
||||
const landingPageOptions: LandingPage[] = ['Home', 'Library']
|
||||
|
||||
const messages = defineMessages({
|
||||
startupAndNavigationTitle: {
|
||||
id: 'app.behavior-settings.startup-and-navigation.title',
|
||||
defaultMessage: 'Startup and navigation',
|
||||
},
|
||||
contentTitle: {
|
||||
id: 'app.behavior-settings.content.title',
|
||||
defaultMessage: 'Home and content',
|
||||
},
|
||||
confirmationsTitle: {
|
||||
id: 'app.behavior-settings.confirmations.title',
|
||||
defaultMessage: 'Confirmations',
|
||||
},
|
||||
minimizeLauncherTitle: {
|
||||
id: 'app.appearance-settings.minimize-launcher.title',
|
||||
defaultMessage: 'Minimize app',
|
||||
},
|
||||
minimizeLauncherDescription: {
|
||||
id: 'app.appearance-settings.minimize-launcher.description',
|
||||
defaultMessage: 'Minimize Modrinth App when Minecraft starts.',
|
||||
},
|
||||
defaultLandingPageTitle: {
|
||||
id: 'app.appearance-settings.default-landing-page.title',
|
||||
defaultMessage: 'Default landing page',
|
||||
},
|
||||
defaultLandingPageDescription: {
|
||||
id: 'app.appearance-settings.default-landing-page.description',
|
||||
defaultMessage: 'Choose the page shown when Modrinth App opens.',
|
||||
},
|
||||
defaultLandingPageHome: {
|
||||
id: 'app.appearance-settings.default-landing-page.home',
|
||||
defaultMessage: 'Home',
|
||||
},
|
||||
defaultLandingPageLibrary: {
|
||||
id: 'app.appearance-settings.default-landing-page.library',
|
||||
defaultMessage: 'Library',
|
||||
},
|
||||
toggleSidebarTitle: {
|
||||
id: 'app.appearance-settings.toggle-sidebar.title',
|
||||
defaultMessage: 'Hide right sidebar',
|
||||
},
|
||||
toggleSidebarDescription: {
|
||||
id: 'app.appearance-settings.toggle-sidebar.description',
|
||||
defaultMessage: 'Hide the right sidebar by default and add a button to show or hide it.',
|
||||
},
|
||||
jumpBackIntoWorldsTitle: {
|
||||
id: 'app.appearance-settings.jump-back-into-worlds.title',
|
||||
defaultMessage: 'Jump back into worlds',
|
||||
},
|
||||
jumpBackIntoWorldsDescription: {
|
||||
id: 'app.appearance-settings.jump-back-into-worlds.description',
|
||||
defaultMessage: 'Show recently played worlds in the "Jump back in" section on the Home page.',
|
||||
},
|
||||
showPlayTimeTitle: {
|
||||
id: 'app.appearance-settings.show-play-time.title',
|
||||
defaultMessage: 'Show play time',
|
||||
},
|
||||
showPlayTimeDescription: {
|
||||
id: 'app.appearance-settings.show-play-time.description',
|
||||
defaultMessage: `Show how long you've played each instance.`,
|
||||
},
|
||||
hideNametagTitle: {
|
||||
id: 'app.appearance-settings.hide-nametag.title',
|
||||
defaultMessage: 'Hide nametag',
|
||||
},
|
||||
hideNametagDescription: {
|
||||
id: 'app.appearance-settings.hide-nametag.description',
|
||||
defaultMessage: 'Hide your username above the player preview on the Skin selector page.',
|
||||
},
|
||||
unknownPackWarningTitle: {
|
||||
id: 'app.appearance-settings.unknown-pack-warning.title',
|
||||
defaultMessage: 'Warn me before installing unknown modpacks',
|
||||
},
|
||||
unknownPackWarningDescription: {
|
||||
id: 'app.appearance-settings.unknown-pack-warning.description',
|
||||
defaultMessage:
|
||||
"Show a safety warning before installing a Modrinth Pack (.mrpack) that isn't hosted on Modrinth.",
|
||||
},
|
||||
skipNonEssentialWarningsTitle: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.title',
|
||||
defaultMessage: 'Skip non-essential warnings',
|
||||
},
|
||||
skipNonEssentialWarningsDescription: {
|
||||
id: 'app.appearance-settings.skip-non-essential-warnings.description',
|
||||
defaultMessage:
|
||||
'Skip confirmations for low-risk actions such as duplicate installs, normal content deletion, bulk updates, unlinking, and repairs. Warnings for dangerous actions are always shown.',
|
||||
},
|
||||
})
|
||||
|
||||
function formatLandingPageLabel(page: LandingPage) {
|
||||
switch (page) {
|
||||
case 'Home':
|
||||
return formatMessage(messages.defaultLandingPageHome)
|
||||
case 'Library':
|
||||
return formatMessage(messages.defaultLandingPageLibrary)
|
||||
}
|
||||
}
|
||||
|
||||
const settings = ref(await get())
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<section>
|
||||
<h2 class="m-0 text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.startupAndNavigationTitle) }}
|
||||
</h2>
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.defaultLandingPageTitle) }}
|
||||
</h3>
|
||||
<Chips
|
||||
v-model="settings.default_page"
|
||||
:items="landingPageOptions"
|
||||
:format-label="formatLandingPageLabel"
|
||||
:capitalize="false"
|
||||
:aria-label="formatMessage(messages.defaultLandingPageTitle)"
|
||||
/>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.defaultLandingPageDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.minimizeLauncherTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.minimizeLauncherDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle id="minimize-launcher" v-model="settings.hide_on_process_start" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.toggleSidebarTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.toggleSidebarDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="toggle-sidebar"
|
||||
:model-value="settings.toggle_sidebar"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
settings.toggle_sidebar = !!e
|
||||
themeStore.toggleSidebar = settings.toggle_sidebar
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8 border-0 border-t border-solid border-divider pt-6">
|
||||
<h2 class="m-0 text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.contentTitle) }}
|
||||
</h2>
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.jumpBackIntoWorldsTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.jumpBackIntoWorldsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="jump-back-into-worlds"
|
||||
:model-value="themeStore.getFeatureFlag(worldsInHomeFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(worldsInHomeFlag)
|
||||
themeStore.featureFlags[worldsInHomeFlag] = newValue
|
||||
settings.feature_flags[worldsInHomeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.showPlayTimeTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.showPlayTimeDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="show-play-time"
|
||||
:model-value="themeStore.getFeatureFlag(showPlayTimeFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(showPlayTimeFlag)
|
||||
themeStore.featureFlags[showPlayTimeFlag] = newValue
|
||||
settings.feature_flags[showPlayTimeFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.hideNametagTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">{{ formatMessage(messages.hideNametagDescription) }}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="hide-nametag-skins-page"
|
||||
:model-value="themeStore.hideNametagSkinsPage"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
themeStore.hideNametagSkinsPage = !!e
|
||||
settings.hide_nametag_skins_page = themeStore.hideNametagSkinsPage
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8 border-0 border-t border-solid border-divider pt-6">
|
||||
<h2 class="m-0 text-xl font-semibold text-contrast">
|
||||
{{ formatMessage(messages.confirmationsTitle) }}
|
||||
</h2>
|
||||
<div class="mt-4 flex flex-col gap-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.unknownPackWarningTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.unknownPackWarningDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="warn-before-installing-unknown-modpacks"
|
||||
:model-value="!themeStore.getFeatureFlag(skipUnknownPackWarningFlag)"
|
||||
@update:model-value="
|
||||
(e) => {
|
||||
const warnBeforeUnknownPackInstall = !!e
|
||||
const skipUnknownPackWarning = !warnBeforeUnknownPackInstall
|
||||
themeStore.featureFlags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
|
||||
settings.feature_flags[skipUnknownPackWarningFlag] = skipUnknownPackWarning
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.skipNonEssentialWarningsTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.skipNonEssentialWarningsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="skip-non-essential-warnings"
|
||||
:model-value="themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(skipNonEssentialWarningsFlag)
|
||||
themeStore.featureFlags[skipNonEssentialWarningsFlag] = newValue
|
||||
settings.feature_flags[skipNonEssentialWarningsFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
+9
-10
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, Toggle } from '@modrinth/ui'
|
||||
import { Button, Toggle } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
@@ -25,7 +25,7 @@ watch(
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-2.5 min-w-[600px]">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<div v-for="option in options" :key="option" class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast capitalize">
|
||||
@@ -33,14 +33,13 @@ watch(
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ButtonStyled type="transparent">
|
||||
<button
|
||||
:disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
|
||||
@click="setFeatureFlag(option, DEFAULT_FEATURE_FLAGS[option])"
|
||||
>
|
||||
Reset to default
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="quiet"
|
||||
:disabled="themeStore.getFeatureFlag(option) === DEFAULT_FEATURE_FLAGS[option]"
|
||||
@click="setFeatureFlag(option, DEFAULT_FEATURE_FLAGS[option])"
|
||||
>
|
||||
Reset to default
|
||||
</Button>
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="themeStore.getFeatureFlag(option)"
|
||||
+4
-1
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
Admonition,
|
||||
AutoLink,
|
||||
commonSettingsMessages,
|
||||
IntlFormatted,
|
||||
LanguageSelector,
|
||||
languageSelectorMessages,
|
||||
@@ -43,7 +44,9 @@ async function onLocaleChange(newLocale: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">Language</h2>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(commonSettingsMessages.language) }}
|
||||
</h2>
|
||||
|
||||
<Admonition type="warning" class="mt-2 mb-4">
|
||||
{{ formatMessage(languageSelectorMessages.languageWarning, { platform }) }}
|
||||
@@ -0,0 +1,333 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
Slider,
|
||||
StyledInput,
|
||||
Toggle,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
fullscreenTitle: {
|
||||
id: 'app.settings.default-instance-options.fullscreen.title',
|
||||
defaultMessage: 'Fullscreen',
|
||||
},
|
||||
fullscreenDescription: {
|
||||
id: 'app.settings.default-instance-options.fullscreen.description',
|
||||
defaultMessage: 'Start instances in fullscreen by updating their options.txt file.',
|
||||
},
|
||||
widthTitle: {
|
||||
id: 'app.settings.default-instance-options.width.title',
|
||||
defaultMessage: 'Width',
|
||||
},
|
||||
widthDescription: {
|
||||
id: 'app.settings.default-instance-options.width.description',
|
||||
defaultMessage: 'The width of the game window when launched.',
|
||||
},
|
||||
widthPlaceholder: {
|
||||
id: 'app.settings.default-instance-options.width.placeholder',
|
||||
defaultMessage: 'Enter width...',
|
||||
},
|
||||
heightTitle: {
|
||||
id: 'app.settings.default-instance-options.height.title',
|
||||
defaultMessage: 'Height',
|
||||
},
|
||||
heightDescription: {
|
||||
id: 'app.settings.default-instance-options.height.description',
|
||||
defaultMessage: 'The height of the game window when launched.',
|
||||
},
|
||||
heightPlaceholder: {
|
||||
id: 'app.settings.default-instance-options.height.placeholder',
|
||||
defaultMessage: 'Enter height...',
|
||||
},
|
||||
memoryAllocationTitle: {
|
||||
id: 'app.settings.default-instance-options.memory-allocation.title',
|
||||
defaultMessage: 'Memory allocation',
|
||||
},
|
||||
memoryAllocationDescription: {
|
||||
id: 'app.settings.default-instance-options.memory-allocation.description',
|
||||
defaultMessage: 'Maximum memory available to each instance.',
|
||||
},
|
||||
javaArgumentsTitle: {
|
||||
id: 'app.settings.default-instance-options.java-arguments.title',
|
||||
defaultMessage: 'Java arguments',
|
||||
},
|
||||
javaArgumentsPlaceholder: {
|
||||
id: 'app.settings.default-instance-options.java-arguments.placeholder',
|
||||
defaultMessage: 'Enter Java arguments...',
|
||||
},
|
||||
javaArgumentsDescription: {
|
||||
id: 'app.settings.default-instance-options.java-arguments.description',
|
||||
defaultMessage: 'Arguments passed to Java when launching an instance.',
|
||||
},
|
||||
environmentVariablesTitle: {
|
||||
id: 'app.settings.default-instance-options.environment-variables.title',
|
||||
defaultMessage: 'Environment variables',
|
||||
},
|
||||
environmentVariablesPlaceholder: {
|
||||
id: 'app.settings.default-instance-options.environment-variables.placeholder',
|
||||
defaultMessage: 'Enter environment variables...',
|
||||
},
|
||||
environmentVariablesDescription: {
|
||||
id: 'app.settings.default-instance-options.environment-variables.description',
|
||||
defaultMessage: 'Environment variables set when launching an instance.',
|
||||
},
|
||||
preLaunchHookTitle: {
|
||||
id: 'app.settings.default-instance-options.pre-launch-hook.title',
|
||||
defaultMessage: 'Pre-launch hook',
|
||||
},
|
||||
preLaunchHookPlaceholder: {
|
||||
id: 'app.settings.default-instance-options.pre-launch-hook.placeholder',
|
||||
defaultMessage: 'Enter pre-launch command...',
|
||||
},
|
||||
preLaunchHookDescription: {
|
||||
id: 'app.settings.default-instance-options.pre-launch-hook.description',
|
||||
defaultMessage: 'Runs before the instance starts.',
|
||||
},
|
||||
wrapperHookTitle: {
|
||||
id: 'app.settings.default-instance-options.wrapper-hook.title',
|
||||
defaultMessage: 'Wrapper hook',
|
||||
},
|
||||
wrapperHookPlaceholder: {
|
||||
id: 'app.settings.default-instance-options.wrapper-hook.placeholder',
|
||||
defaultMessage: 'Enter wrapper command...',
|
||||
},
|
||||
wrapperHookDescription: {
|
||||
id: 'app.settings.default-instance-options.wrapper-hook.description',
|
||||
defaultMessage: 'Command used to wrap the Minecraft launch process.',
|
||||
},
|
||||
postExitHookTitle: {
|
||||
id: 'app.settings.default-instance-options.post-exit-hook.title',
|
||||
defaultMessage: 'Post-exit hook',
|
||||
},
|
||||
postExitHookPlaceholder: {
|
||||
id: 'app.settings.default-instance-options.post-exit-hook.placeholder',
|
||||
defaultMessage: 'Enter post-exit command...',
|
||||
},
|
||||
postExitHookDescription: {
|
||||
id: 'app.settings.default-instance-options.post-exit-hook.description',
|
||||
defaultMessage: 'Runs after the game closes.',
|
||||
},
|
||||
})
|
||||
|
||||
const fetchSettings = await get()
|
||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||
fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).join(' ')
|
||||
|
||||
const settings = ref(fetchSettings)
|
||||
|
||||
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||
maxMemory: number
|
||||
snapPoints: number[]
|
||||
}
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||
|
||||
setSettings.extra_launch_args = setSettings.launchArgs.trim().split(/\s+/).filter(Boolean)
|
||||
setSettings.custom_env_vars = setSettings.envVars
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.map((x) => x.split('=').filter(Boolean))
|
||||
|
||||
if (!setSettings.hooks.pre_launch) {
|
||||
setSettings.hooks.pre_launch = null
|
||||
}
|
||||
if (!setSettings.hooks.wrapper) {
|
||||
setSettings.hooks.wrapper = null
|
||||
}
|
||||
if (!setSettings.hooks.post_exit) {
|
||||
setSettings.hooks.post_exit = null
|
||||
}
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.fullscreenTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.fullscreenDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Toggle id="fullscreen" v-model="settings.force_fullscreen" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.widthTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.widthDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<StyledInput
|
||||
id="width"
|
||||
v-model="settings.game_resolution[0]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.widthPlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.heightTitle) }}
|
||||
</h3>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.heightDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<StyledInput
|
||||
id="height"
|
||||
v-model="settings.game_resolution[1]"
|
||||
:disabled="settings.force_fullscreen"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
:placeholder="formatMessage(messages.heightPlaceholder)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-6 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.memoryAllocationTitle) }}
|
||||
</h2>
|
||||
<Slider
|
||||
id="max-memory"
|
||||
v-model="settings.memory.maximum"
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<p class="m-0 mt-1 leading-tight">
|
||||
{{ formatMessage(messages.memoryAllocationDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.javaArgumentsTitle) }}
|
||||
</h2>
|
||||
<StyledInput
|
||||
id="java-args"
|
||||
v-model="settings.launchArgs"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.javaArgumentsPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.javaArgumentsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.environmentVariablesTitle) }}
|
||||
</h2>
|
||||
<StyledInput
|
||||
id="env-vars"
|
||||
v-model="settings.envVars"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.environmentVariablesPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.environmentVariablesDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-6 bg-button-border border-none h-[1px]" />
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.preLaunchHookTitle) }}
|
||||
</h3>
|
||||
<StyledInput
|
||||
id="pre-launch"
|
||||
v-model="settings.hooks.pre_launch"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.preLaunchHookPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.preLaunchHookDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.wrapperHookTitle) }}
|
||||
</h3>
|
||||
<StyledInput
|
||||
id="wrapper"
|
||||
v-model="settings.hooks.wrapper"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.wrapperHookPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.wrapperHookDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h3 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.postExitHookTitle) }}
|
||||
</h3>
|
||||
<StyledInput
|
||||
id="post-exit"
|
||||
v-model="settings.hooks.post_exit"
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
:placeholder="formatMessage(messages.postExitHookPlaceholder)"
|
||||
wrapper-class="w-full"
|
||||
/>
|
||||
<p class="m-0 leading-tight">
|
||||
{{ formatMessage(messages.postExitHookDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+10
-2
@@ -1,11 +1,19 @@
|
||||
<script setup>
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
javaLocation: {
|
||||
id: 'app.settings.java-installations.location.title',
|
||||
defaultMessage: 'Java {version, number} location',
|
||||
},
|
||||
})
|
||||
|
||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||
async function updateJavaVersion(version) {
|
||||
@@ -28,7 +36,7 @@ async function updateJavaVersion(version) {
|
||||
class="flex flex-col gap-2.5"
|
||||
>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast" :class="{ 'mt-4': index !== 0 }">
|
||||
Java {{ javaVersion }} location
|
||||
{{ formatMessage(messages.javaLocation, { version: javaVersion }) }}
|
||||
</h2>
|
||||
<JavaSelector
|
||||
:id="'java-selector-' + javaVersion"
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
<script setup>
|
||||
import { BoxIcon, FolderOpenIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
injectNotificationManager,
|
||||
Slider,
|
||||
StyledInput,
|
||||
Toggle,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { showAppDbBackupsFolder } from '@/helpers/utils.js'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const settings = ref(await get())
|
||||
const purgeCacheConfirmModal = ref(null)
|
||||
const alwaysShowCopyDetailsFlag = 'always_show_copy_details'
|
||||
|
||||
const messages = defineMessages({
|
||||
appDirectoryTitle: {
|
||||
id: 'app.settings.resource-management.app-directory.title',
|
||||
defaultMessage: 'App directory',
|
||||
},
|
||||
appDirectoryDescription: {
|
||||
id: 'app.settings.resource-management.app-directory.description',
|
||||
defaultMessage:
|
||||
'Where Modrinth App stores instances and other files. Changes take effect after restarting the app.',
|
||||
},
|
||||
selectAppDirectory: {
|
||||
id: 'app.settings.resource-management.app-directory.select',
|
||||
defaultMessage: 'Select a new app directory',
|
||||
},
|
||||
browseAppDirectory: {
|
||||
id: 'app.settings.resource-management.app-directory.browse',
|
||||
defaultMessage: 'Browse for an app directory',
|
||||
},
|
||||
appCacheTitle: {
|
||||
id: 'app.settings.resource-management.app-cache.title',
|
||||
defaultMessage: 'App cache',
|
||||
},
|
||||
purgeCache: {
|
||||
id: 'app.settings.resource-management.app-cache.purge',
|
||||
defaultMessage: 'Purge cache',
|
||||
},
|
||||
purgeCacheConfirmTitle: {
|
||||
id: 'app.settings.resource-management.app-cache.confirm.title',
|
||||
defaultMessage: 'Purge the app cache?',
|
||||
},
|
||||
purgeCacheConfirmDescription: {
|
||||
id: 'app.settings.resource-management.app-cache.confirm.description',
|
||||
defaultMessage: 'The app may load more slowly until the cache is rebuilt.',
|
||||
},
|
||||
appCacheDescription: {
|
||||
id: 'app.settings.resource-management.app-cache.description',
|
||||
defaultMessage:
|
||||
'Clear cached data and download it again from Modrinth. The app may load more slowly until the cache is rebuilt.',
|
||||
},
|
||||
maximumConcurrentDownloadsTitle: {
|
||||
id: 'app.settings.resource-management.maximum-concurrent-downloads.title',
|
||||
defaultMessage: 'Maximum concurrent downloads',
|
||||
},
|
||||
maximumConcurrentDownloadsDescription: {
|
||||
id: 'app.settings.resource-management.maximum-concurrent-downloads.description',
|
||||
defaultMessage:
|
||||
'Number of files the app can download at once. Lower this if downloads are unreliable on your connection. Requires an app restart.',
|
||||
},
|
||||
maximumConcurrentWritesTitle: {
|
||||
id: 'app.settings.resource-management.maximum-concurrent-writes.title',
|
||||
defaultMessage: 'Maximum concurrent writes',
|
||||
},
|
||||
maximumConcurrentWritesDescription: {
|
||||
id: 'app.settings.resource-management.maximum-concurrent-writes.description',
|
||||
defaultMessage:
|
||||
'Number of files the app can write to disk at once. Lower this if you frequently encounter I/O errors. Requires an app restart.',
|
||||
},
|
||||
alwaysShowCopyDetailsTitle: {
|
||||
id: 'app.settings.resource-management.always-show-copy-details.title',
|
||||
defaultMessage: 'Always show copy details',
|
||||
},
|
||||
alwaysShowCopyDetailsDescription: {
|
||||
id: 'app.settings.resource-management.always-show-copy-details.description',
|
||||
defaultMessage:
|
||||
'Show the Copy details action while an install is queued or running. It is always available for failed or interrupted installs.',
|
||||
},
|
||||
appDatabaseBackupsTitle: {
|
||||
id: 'app.settings.resource-management.app-database-backups.title',
|
||||
defaultMessage: 'App database backups',
|
||||
},
|
||||
openBackupsFolder: {
|
||||
id: 'app.settings.resource-management.app-database-backups.open-folder',
|
||||
defaultMessage: 'Open backups folder',
|
||||
},
|
||||
appDatabaseBackupsDescription: {
|
||||
id: 'app.settings.resource-management.app-database-backups.description',
|
||||
defaultMessage:
|
||||
'Backups of important app data are stored here in case you need to recover them later.',
|
||||
},
|
||||
})
|
||||
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
const setSettings = JSON.parse(JSON.stringify(settings.value))
|
||||
|
||||
if (!setSettings.custom_dir) {
|
||||
setSettings.custom_dir = null
|
||||
}
|
||||
|
||||
await set(setSettings)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
async function purgeCache() {
|
||||
await purge_cache_types([
|
||||
'project',
|
||||
'project_v3',
|
||||
'version',
|
||||
'user',
|
||||
'team',
|
||||
'organization',
|
||||
'file',
|
||||
'loader_manifest',
|
||||
'minecraft_manifest',
|
||||
'categories',
|
||||
'report_types',
|
||||
'loaders',
|
||||
'game_versions',
|
||||
'donation_platforms',
|
||||
'file_hash',
|
||||
'file_update',
|
||||
'search_results',
|
||||
'search_results_v3',
|
||||
]).catch(handleError)
|
||||
}
|
||||
|
||||
function handlePurgeCacheClick() {
|
||||
if (themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
void purgeCache()
|
||||
return
|
||||
}
|
||||
|
||||
purgeCacheConfirmModal.value?.show()
|
||||
}
|
||||
|
||||
async function openDbBackupsFolder() {
|
||||
await showAppDbBackupsFolder().catch(handleError)
|
||||
}
|
||||
|
||||
async function findLauncherDir() {
|
||||
const newDir = await open({
|
||||
multiple: false,
|
||||
directory: true,
|
||||
title: formatMessage(messages.selectAppDirectory),
|
||||
})
|
||||
|
||||
if (newDir) {
|
||||
settings.value.custom_dir = newDir
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.appDirectoryTitle) }}
|
||||
</h2>
|
||||
<StyledInput
|
||||
id="appDir"
|
||||
v-model="settings.custom_dir"
|
||||
:icon="BoxIcon"
|
||||
type="text"
|
||||
wrapper-class="w-full"
|
||||
>
|
||||
<template #right>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.browseAppDirectory)"
|
||||
:label="formatMessage(messages.browseAppDirectory)"
|
||||
class="ml-1.5"
|
||||
@click="findLauncherDir"
|
||||
>
|
||||
<FolderSearchIcon aria-hidden="true" />
|
||||
</IconButton>
|
||||
</template>
|
||||
</StyledInput>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
{{ formatMessage(messages.appDirectoryDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.alwaysShowCopyDetailsTitle) }}
|
||||
</h2>
|
||||
<p class="m-0 mt-1">
|
||||
{{ formatMessage(messages.alwaysShowCopyDetailsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
id="always-show-copy-details"
|
||||
:model-value="themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag(alwaysShowCopyDetailsFlag)
|
||||
themeStore.featureFlags[alwaysShowCopyDetailsFlag] = newValue
|
||||
settings.feature_flags[alwaysShowCopyDetailsFlag] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<ConfirmModalWrapper
|
||||
ref="purgeCacheConfirmModal"
|
||||
:title="formatMessage(messages.purgeCacheConfirmTitle)"
|
||||
:description="formatMessage(messages.purgeCacheConfirmDescription)"
|
||||
:has-to-type="false"
|
||||
:proceed-label="formatMessage(messages.purgeCache)"
|
||||
:show-ad-on-close="false"
|
||||
@proceed="purgeCache"
|
||||
/>
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.appCacheTitle) }}
|
||||
</h2>
|
||||
<Button id="purge-cache" class="w-fit" @click="handlePurgeCacheClick">
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.purgeCache) }}
|
||||
</Button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
{{ formatMessage(messages.appCacheDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="m-0 text-lg font-semibold text-contrast mt-4">
|
||||
{{ formatMessage(messages.maximumConcurrentDownloadsTitle) }}
|
||||
</h2>
|
||||
<Slider
|
||||
id="max-downloads"
|
||||
v-model="settings.max_concurrent_downloads"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="1"
|
||||
/>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
{{ formatMessage(messages.maximumConcurrentDownloadsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.maximumConcurrentWritesTitle) }}
|
||||
</h2>
|
||||
<Slider
|
||||
id="max-writes"
|
||||
v-model="settings.max_concurrent_writes"
|
||||
:min="1"
|
||||
:max="50"
|
||||
:step="1"
|
||||
/>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
{{ formatMessage(messages.maximumConcurrentWritesDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<h2 class="mt-0 m-0 text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.appDatabaseBackupsTitle) }}
|
||||
</h2>
|
||||
<Button id="open-db-backups-folder" class="w-fit" @click="openDbBackupsFolder">
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.openBackupsFolder) }}
|
||||
</Button>
|
||||
<p class="m-0 leading-tight text-secondary">
|
||||
{{ formatMessage(messages.appDatabaseBackupsDescription) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<NewModal ref="modal" :header="formatMessage(messages.header)" fade="danger" max-width="500px">
|
||||
<Admonition type="critical" :header="formatMessage(messages.admonitionHeader)">
|
||||
<IntlFormatted :message-id="messages.admonitionBody" :values="{ code: inviteCode }">
|
||||
<template #monospace="{ children }">
|
||||
<code class="font-mono"><component :is="() => children" /></code>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</Admonition>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button type="outlined" @click="modal?.hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="red" @click="confirm">
|
||||
<XIcon />
|
||||
{{ formatMessage(messages.revokeButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
NewModal,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const inviteCode = ref('')
|
||||
|
||||
const emit = defineEmits<{
|
||||
revoke: [inviteCode: string]
|
||||
}>()
|
||||
|
||||
function show(code: string) {
|
||||
inviteCode.value = code
|
||||
modal.value?.show()
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
modal.value?.hide()
|
||||
emit('revoke', inviteCode.value)
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
header: {
|
||||
id: 'instance.settings.sharing.revoke-invite.header',
|
||||
defaultMessage: 'Revoke invite',
|
||||
},
|
||||
admonitionHeader: {
|
||||
id: 'instance.settings.sharing.revoke-invite.admonition-header',
|
||||
defaultMessage: 'This action cannot be undone',
|
||||
},
|
||||
admonitionBody: {
|
||||
id: 'instance.settings.sharing.revoke-invite.admonition-body',
|
||||
defaultMessage:
|
||||
'The invite link <monospace>{code}</monospace> will stop working immediately. People who already joined will keep access.',
|
||||
},
|
||||
revokeButton: {
|
||||
id: 'instance.settings.sharing.revoke-invite.confirm',
|
||||
defaultMessage: 'Revoke invite',
|
||||
},
|
||||
})
|
||||
|
||||
defineExpose({ show })
|
||||
</script>
|
||||
+13
-19
@@ -10,24 +10,18 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="handleInstallAnyway">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="handleCancel">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button @click="handleGoToInstance">
|
||||
{{ formatMessage(messages.instance) }}
|
||||
<RightArrowIcon />
|
||||
</Button>
|
||||
<Button type="colored" color="orange" @click="handleInstallAnyway">
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -36,7 +30,7 @@
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, RightArrowIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IntlFormatted,
|
||||
|
||||
+25
-32
@@ -2,13 +2,11 @@
|
||||
<div v-if="canUnpublish" class="flex flex-col gap-2.5">
|
||||
<span class="text-lg font-semibold text-contrast">{{ formatMessage(messages.title) }}</span>
|
||||
<div>
|
||||
<ButtonStyled color="orange">
|
||||
<button :disabled="busy" @click="unpublishModal?.show()">
|
||||
<SpinnerIcon v-if="unpublishing" class="animate-spin" />
|
||||
<UnlinkIcon v-else class="size-5" />
|
||||
{{ formatMessage(unpublishing ? messages.unpublishingButton : messages.unpublishButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="orange" :disabled="busy" @click="unpublishModal?.show()">
|
||||
<SpinnerIcon v-if="unpublishing" class="animate-spin" />
|
||||
<UnlinkIcon v-else class="size-5" />
|
||||
{{ formatMessage(unpublishing ? messages.unpublishingButton : messages.unpublishButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
<span class="text-primary">{{ formatMessage(messages.unpublishDescription) }}</span>
|
||||
</div>
|
||||
@@ -17,13 +15,11 @@
|
||||
formatMessage(messages.linkedTitle)
|
||||
}}</span>
|
||||
<div>
|
||||
<ButtonStyled color="orange">
|
||||
<button :disabled="busy" @click="unlinkModal?.show()">
|
||||
<SpinnerIcon v-if="unlinking" class="animate-spin" />
|
||||
<UnlinkIcon v-else class="size-5" />
|
||||
{{ formatMessage(unlinking ? messages.unlinkingButton : messages.unlinkButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="orange" :disabled="busy" @click="unlinkModal?.show()">
|
||||
<SpinnerIcon v-if="unlinking" class="animate-spin" />
|
||||
<UnlinkIcon v-else class="size-5" />
|
||||
{{ formatMessage(unlinking ? messages.unlinkingButton : messages.unlinkButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
<span class="text-primary">{{ formatMessage(messages.unlinkDescription) }}</span>
|
||||
</div>
|
||||
@@ -39,15 +35,11 @@
|
||||
}}</Admonition>
|
||||
<template #actions
|
||||
><div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined"
|
||||
><button class="!border" @click="unpublishModal?.hide()">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button></ButtonStyled
|
||||
><ButtonStyled color="orange"
|
||||
><button :disabled="busy" @click="confirmUnpublish">
|
||||
<UnlinkIcon />{{ formatMessage(messages.unpublishButton) }}
|
||||
</button></ButtonStyled
|
||||
>
|
||||
<Button type="outlined" class="!border" @click="unpublishModal?.hide()">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }} </Button
|
||||
><Button type="colored" color="orange" :disabled="busy" @click="confirmUnpublish">
|
||||
<UnlinkIcon />{{ formatMessage(messages.unpublishButton) }}
|
||||
</Button>
|
||||
</div></template
|
||||
>
|
||||
</NewModal>
|
||||
@@ -70,15 +62,16 @@
|
||||
</div>
|
||||
<template #actions
|
||||
><div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined"
|
||||
><button class="!border" @click="unlinkModal?.hide()">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button></ButtonStyled
|
||||
><ButtonStyled color="orange"
|
||||
><button :disabled="busy || backupBusy" @click="confirmUnlink">
|
||||
<UnlinkIcon />{{ formatMessage(messages.unlinkButton) }}
|
||||
</button></ButtonStyled
|
||||
<Button type="outlined" class="!border" @click="unlinkModal?.hide()">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }} </Button
|
||||
><Button
|
||||
type="colored"
|
||||
color="orange"
|
||||
:disabled="busy || backupBusy"
|
||||
@click="confirmUnlink"
|
||||
>
|
||||
<UnlinkIcon />{{ formatMessage(messages.unlinkButton) }}
|
||||
</Button>
|
||||
</div></template
|
||||
>
|
||||
</NewModal>
|
||||
@@ -88,7 +81,7 @@
|
||||
import { SpinnerIcon, UnlinkIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
InlineBackupCreator,
|
||||
|
||||
@@ -26,12 +26,13 @@
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex min-w-0 flex-col gap-3 pt-4">
|
||||
<div class="max-h-[292px] overflow-y-auto rounded-[20px]">
|
||||
<div ref="configFileTreeContainer" class="max-h-[292px] overflow-y-auto rounded-[20px]">
|
||||
<FileTreeSelect
|
||||
v-model="selectedConfigPaths"
|
||||
:items="configFileItems"
|
||||
:show-size="false"
|
||||
:show-modified="false"
|
||||
@navigate="scrollConfigFileTreeToTop"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,6 +76,7 @@ const emit = defineEmits<{
|
||||
const { formatMessage } = useVIntl()
|
||||
const { notifySharedInstanceError, notifySharedInstanceUnavailable } = useSharedInstanceErrors()
|
||||
const publishReviewModal = ref<InstanceType<typeof ContentDiffModal>>()
|
||||
const configFileTreeContainer = ref<HTMLElement>()
|
||||
const publishDiffs = ref<ContentDiffItem[]>([])
|
||||
const configFilePaths = ref<string[]>([])
|
||||
const selectedConfigPaths = ref<string[]>([])
|
||||
@@ -132,6 +134,12 @@ async function publishChanges() {
|
||||
}
|
||||
}
|
||||
|
||||
function scrollConfigFileTreeToTop() {
|
||||
if (configFileTreeContainer.value) {
|
||||
configFileTreeContainer.value.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
function finishReview() {
|
||||
if (state.value === 'reviewing') {
|
||||
setState('idle')
|
||||
|
||||
+19
-12
@@ -43,8 +43,9 @@ import type { GameInstance } from '@/helpers/types'
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
const emit = defineEmits<{
|
||||
accepted: []
|
||||
cancel: []
|
||||
complete: []
|
||||
complete: [successful: boolean]
|
||||
report: [event?: MouseEvent]
|
||||
sharedInstanceUnavailable: [reason: SharedInstanceUnavailableReason | null]
|
||||
}>()
|
||||
@@ -56,24 +57,30 @@ const { formatMessage } = useVIntl()
|
||||
const { notifySharedInstanceError } = useSharedInstanceErrors()
|
||||
const diffs = computed<ContentDiffItem[]>(
|
||||
() =>
|
||||
preview.value?.diffs.map((diff) => ({
|
||||
type: diff.type,
|
||||
projectName: diff.projectName ?? undefined,
|
||||
fileName: diff.fileName ?? undefined,
|
||||
currentVersionName: diff.currentVersionName ?? undefined,
|
||||
newVersionName: diff.newVersionName ?? undefined,
|
||||
fileCount: diff.configFileCount ?? undefined,
|
||||
disabled: diff.disabled,
|
||||
external: diff.type === 'added' && !diff.projectId && !!diff.fileName,
|
||||
})) ?? [],
|
||||
preview.value?.diffs
|
||||
// TODO: This is TEMP!!! Hashing needs to be done on backend
|
||||
.filter((diff) => !(diff.type === 'updated' && !diff.projectId && diff.fileName))
|
||||
.map((diff) => ({
|
||||
type: diff.type,
|
||||
projectName: diff.projectName ?? undefined,
|
||||
fileName: diff.fileName ?? undefined,
|
||||
currentVersionName: diff.currentVersionName ?? undefined,
|
||||
newVersionName: diff.newVersionName ?? undefined,
|
||||
fileCount: diff.configFileCount ?? undefined,
|
||||
disabled: diff.disabled,
|
||||
external: diff.type === 'added' && !diff.projectId && !!diff.fileName,
|
||||
})) ?? [],
|
||||
)
|
||||
|
||||
async function update() {
|
||||
let successful = false
|
||||
emit('accepted')
|
||||
try {
|
||||
if (instance.value) {
|
||||
const job = await install_update_shared_instance(instance.value.id)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await onComplete.value()
|
||||
successful = true
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSharedInstanceUnavailableError(error)) {
|
||||
@@ -82,7 +89,7 @@ async function update() {
|
||||
}
|
||||
notifySharedInstanceError(error)
|
||||
} finally {
|
||||
emit('complete')
|
||||
emit('complete', successful)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+140
-49
@@ -64,7 +64,29 @@
|
||||
</div>
|
||||
</Admonition>
|
||||
<p v-else class="m-0 text-primary">
|
||||
{{ formatMessage(messages.inviteWarning) }}
|
||||
<IntlFormatted
|
||||
v-if="creator"
|
||||
:message-id="messages.inviteWarningWithCreator"
|
||||
:values="{ username: creator.username }"
|
||||
>
|
||||
<template #creator="{ children }">
|
||||
<AutoLink :to="creatorProfileLink" class="font-medium text-contrast hover:underline">
|
||||
<Avatar
|
||||
:src="creator.avatarUrl"
|
||||
:alt="creator.username"
|
||||
:tint-by="creator.username"
|
||||
size="24px"
|
||||
circle
|
||||
no-shadow
|
||||
class="mr-1 inline-block align-middle"
|
||||
/>
|
||||
<span><component :is="() => children" /></span>
|
||||
</AutoLink>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
<template v-else>
|
||||
{{ formatMessage(messages.inviteWarning) }}
|
||||
</template>
|
||||
</p>
|
||||
<SharedInstanceInstallSummary
|
||||
:preview="preview"
|
||||
@@ -98,8 +120,17 @@
|
||||
:max-height="240"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="reportOnly" class="flex flex-col gap-2">
|
||||
<Checkbox v-model="deleteInstance" :label="formatMessage(messages.deleteInstance)" />
|
||||
<div v-if="reportOnly || blockTargetUserId" class="flex flex-col gap-2">
|
||||
<Checkbox
|
||||
v-if="reportOnly"
|
||||
v-model="deleteInstance"
|
||||
:label="formatMessage(messages.deleteInstance)"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="blockTargetUserId"
|
||||
v-model="blockUser"
|
||||
:label="formatMessage(messages.blockUser)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -162,53 +193,39 @@
|
||||
{{ formatMessage(messages.reviewedFiles) }}
|
||||
</p>
|
||||
<div v-if="!reportMode" class="flex w-full items-center justify-between gap-2">
|
||||
<ButtonStyled color="red" type="transparent">
|
||||
<button @click="reportMode = true">
|
||||
<ReportIcon />{{ formatMessage(commonMessages.reportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="red" @click="reportMode = true">
|
||||
<ReportIcon />{{ formatMessage(commonMessages.reportButton) }}
|
||||
</Button>
|
||||
<div class="flex items-center gap-2">
|
||||
<template v-if="hasExternalFiles">
|
||||
<ButtonStyled type="transparent" color="orange">
|
||||
<button @click="accept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="handleCancel">
|
||||
<BanIcon />{{ formatMessage(messages.dontInstall) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" color="orange" @click="accept">
|
||||
{{ formatMessage(messages.installAnyway) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="handleCancel">
|
||||
<BanIcon />{{ formatMessage(messages.dontInstall) }}
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="accept">
|
||||
<DownloadIcon />{{ formatMessage(messages.installButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" class="!border" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" @click="accept">
|
||||
<DownloadIcon />{{ formatMessage(messages.installButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="reportMode" #actions>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled type="outlined">
|
||||
<button class="!border" :disabled="submitLoading" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!canSubmitReport" @click="submitReport">
|
||||
<SpinnerIcon v-if="submitLoading" class="animate-spin" />
|
||||
<SendIcon v-else />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" class="!border" :disabled="submitLoading" @click="handleCancel">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" :disabled="!canSubmitReport" @click="submitReport">
|
||||
<SpinnerIcon v-if="submitLoading" class="animate-spin" />
|
||||
<SendIcon v-else />
|
||||
{{ formatMessage(commonMessages.reportButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -223,15 +240,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { BanIcon, DownloadIcon, ReportIcon, SendIcon, SpinnerIcon, XIcon } from '@modrinth/assets'
|
||||
import { Button } from '@modrinth/ui'
|
||||
import {
|
||||
Admonition,
|
||||
AutoLink,
|
||||
ButtonStyled,
|
||||
Avatar,
|
||||
blockedUsersQueryKey,
|
||||
Checkbox,
|
||||
Combobox,
|
||||
type ComboboxOption,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectAuth,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
IntlFormatted,
|
||||
@@ -243,12 +263,16 @@ import {
|
||||
useScrollIndicator,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import { config } from '@/config'
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
import { create_report } from '@/helpers/reports'
|
||||
import { block_user } from '@/helpers/users'
|
||||
|
||||
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
|
||||
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
|
||||
@@ -258,11 +282,17 @@ type ExternalFileRow = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
type SharedInstanceCreator = {
|
||||
id: string | null
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
}
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
const contentModal = ref<InstanceType<typeof ModpackContentModal>>()
|
||||
const externalFileTable = ref<HTMLElement | null>(null)
|
||||
const preview = ref<SharedInstanceInstallPreview | null>(null)
|
||||
const creator = ref<SharedInstanceCreator | null>(null)
|
||||
const install = ref<() => void | Promise<void>>(() => {})
|
||||
const reportMode = ref(false)
|
||||
const reportOnly = ref(false)
|
||||
@@ -270,13 +300,17 @@ type ReportReason = 'malicious' | 'inappropriate' | 'spam'
|
||||
const reportReason = ref<ReportReason>('malicious')
|
||||
const additionalContext = ref('')
|
||||
const deleteInstance = ref(true)
|
||||
const blockUser = ref(true)
|
||||
const blockTargetUserId = ref<string | null>(null)
|
||||
const submitLoading = ref(false)
|
||||
const uploadedImageIDs = ref<string[]>([])
|
||||
const emit = defineEmits<{
|
||||
reported: [deleteInstance: boolean]
|
||||
}>()
|
||||
const { formatMessage } = useVIntl()
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const queryClient = useQueryClient()
|
||||
const { addNotification, handleError } = injectNotificationManager()
|
||||
const { load } = useSharedInstancePreviewContent()
|
||||
const {
|
||||
@@ -302,6 +336,12 @@ const reportReasonOptions = computed<ComboboxOption<ReportReason>[]>(() => [
|
||||
const canSubmitReport = computed(
|
||||
() => Boolean(preview.value && additionalContext.value.trim()) && !submitLoading.value,
|
||||
)
|
||||
const creatorProfileLink = computed(() => {
|
||||
const username = creator.value?.username
|
||||
return username
|
||||
? () => openUrl(`${config.siteUrl}/user/${encodeURIComponent(username)}`)
|
||||
: undefined
|
||||
})
|
||||
|
||||
async function accept() {
|
||||
hide()
|
||||
@@ -329,13 +369,39 @@ async function submitReport() {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const uploadedImages = uploadedImageIDs.value.slice(-10)
|
||||
await create_report({
|
||||
report_type: reportReason.value,
|
||||
item_type: 'shared-instance',
|
||||
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
|
||||
body,
|
||||
uploaded_images: uploadedImages,
|
||||
})
|
||||
const blockTarget = blockUser.value ? blockTargetUserId.value : null
|
||||
const [reportResult, blockResult] = await Promise.allSettled([
|
||||
create_report({
|
||||
report_type: reportReason.value,
|
||||
item_type: 'shared-instance',
|
||||
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
|
||||
body,
|
||||
uploaded_images: uploadedImages,
|
||||
}),
|
||||
blockTarget ? block_user(blockTarget) : Promise.resolve(),
|
||||
])
|
||||
|
||||
if (blockTarget) {
|
||||
if (blockResult.status === 'fulfilled') {
|
||||
blockUser.value = false
|
||||
const authUserId = auth.user.value?.id
|
||||
if (authUserId) {
|
||||
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
|
||||
blockedUsersQueryKey(authUserId),
|
||||
(blockedUsers = []) =>
|
||||
blockedUsers.includes(blockTarget) ? blockedUsers : [...blockedUsers, blockTarget],
|
||||
)
|
||||
}
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: formatMessage(messages.userBlocked),
|
||||
})
|
||||
} else {
|
||||
handleError(toError(blockResult.reason))
|
||||
}
|
||||
}
|
||||
|
||||
if (reportResult.status === 'rejected') throw reportResult.reason
|
||||
|
||||
const shouldDeleteInstance = reportOnly.value && deleteInstance.value
|
||||
hide()
|
||||
@@ -381,6 +447,7 @@ function handleCancel() {
|
||||
}
|
||||
function handleHide() {
|
||||
resetReportState()
|
||||
creator.value = null
|
||||
show_ads_window()
|
||||
}
|
||||
function resetReportState() {
|
||||
@@ -389,20 +456,31 @@ function resetReportState() {
|
||||
reportReason.value = 'malicious'
|
||||
additionalContext.value = ''
|
||||
deleteInstance.value = true
|
||||
blockUser.value = true
|
||||
blockTargetUserId.value = null
|
||||
submitLoading.value = false
|
||||
uploadedImageIDs.value = []
|
||||
}
|
||||
function show(
|
||||
previewValue: SharedInstanceInstallPreview,
|
||||
installValue: () => void | Promise<void>,
|
||||
creatorValue?: SharedInstanceCreator,
|
||||
event?: MouseEvent,
|
||||
) {
|
||||
resetReportState()
|
||||
creator.value = creatorValue ?? null
|
||||
blockTargetUserId.value = creatorValue?.id ?? null
|
||||
install.value = installValue
|
||||
showPreview(previewValue, event)
|
||||
}
|
||||
function showReport(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) {
|
||||
function showReport(
|
||||
previewValue: SharedInstanceInstallPreview,
|
||||
blockTargetUserIdValue?: string | null,
|
||||
event?: MouseEvent,
|
||||
) {
|
||||
resetReportState()
|
||||
creator.value = null
|
||||
blockTargetUserId.value = blockTargetUserIdValue ?? null
|
||||
reportMode.value = true
|
||||
reportOnly.value = true
|
||||
install.value = () => {}
|
||||
@@ -437,6 +515,11 @@ const messages = defineMessages({
|
||||
defaultMessage:
|
||||
'This invite was created by another Modrinth user, not Modrinth. Only accept invites from people you trust.',
|
||||
},
|
||||
inviteWarningWithCreator: {
|
||||
id: 'app.modal.install-to-play.invite-warning-with-creator',
|
||||
defaultMessage:
|
||||
'This invite was created by <creator>{username}</creator>, not Modrinth. Only accept invites from people you trust.',
|
||||
},
|
||||
reportDescription: {
|
||||
id: 'app.modal.install-to-play.report-description',
|
||||
defaultMessage:
|
||||
@@ -492,6 +575,14 @@ const messages = defineMessages({
|
||||
id: 'app.modal.install-to-play.delete-instance',
|
||||
defaultMessage: 'Delete instance',
|
||||
},
|
||||
blockUser: {
|
||||
id: 'app.modal.install-to-play.block-user',
|
||||
defaultMessage: 'Block user',
|
||||
},
|
||||
userBlocked: {
|
||||
id: 'app.modal.install-to-play.user-blocked',
|
||||
defaultMessage: 'User blocked',
|
||||
},
|
||||
unknownFilesWarning: {
|
||||
id: 'app.modal.install-to-play.unknown-files-warning',
|
||||
defaultMessage: 'Unknown files warning',
|
||||
|
||||
+5
-7
@@ -4,12 +4,10 @@
|
||||
<span class="font-semibold text-contrast">{{
|
||||
heading ?? formatMessage(messages.sharedInstance)
|
||||
}}</span>
|
||||
<ButtonStyled type="transparent">
|
||||
<button @click="emit('viewContents')">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="quiet" @click="emit('viewContents')">
|
||||
<EyeIcon />
|
||||
{{ formatMessage(messages.viewContents) }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 rounded-2xl bg-surface-2 p-3">
|
||||
<Avatar
|
||||
@@ -34,7 +32,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { EyeIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, defineMessages, formatLoader, useVIntl } from '@modrinth/ui'
|
||||
import { Avatar, Button, defineMessages, formatLoader, useVIntl } from '@modrinth/ui'
|
||||
import { computed, toRefs } from 'vue'
|
||||
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
|
||||
+13
-2
@@ -1,7 +1,7 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import type { ContentItem } from '@modrinth/ui'
|
||||
|
||||
import { get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import { get_project, get_project_many, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import type { SharedInstanceInstallPreview } from '@/helpers/install'
|
||||
|
||||
type VersionDependency = Labrinth.Versions.v2.Dependency & { version_id?: string }
|
||||
@@ -20,7 +20,18 @@ export function useSharedInstancePreviewContent() {
|
||||
async function modpackContentItems(preview: SharedInstanceInstallPreview) {
|
||||
if (!preview.modpackVersionId) return []
|
||||
const version = await get_version(preview.modpackVersionId, 'must_revalidate')
|
||||
return await contentItemsFromDependencies(version?.dependencies ?? [])
|
||||
if (!version) return []
|
||||
|
||||
const [project, contentItems] = await Promise.all([
|
||||
get_project(version.project_id, 'must_revalidate'),
|
||||
contentItemsFromDependencies(version.dependencies ?? []),
|
||||
])
|
||||
if (!project) return contentItems
|
||||
|
||||
return contentItems.map((item) => ({
|
||||
...item,
|
||||
source: { project },
|
||||
}))
|
||||
}
|
||||
|
||||
async function contentItemsFromDependencies(dependencies: Labrinth.Versions.v2.Dependency[]) {
|
||||
|
||||
+2
@@ -28,6 +28,7 @@ const accountRequiredModal = ref<InstanceType<typeof ModrinthAccountRequiredModa
|
||||
const {
|
||||
handleNotification,
|
||||
installFromInviteId,
|
||||
clearNotifications,
|
||||
handleAlreadyInstalledCancel,
|
||||
handleAlreadyInstalledGoToInstance,
|
||||
handleAlreadyInstalledInstallAnyway,
|
||||
@@ -42,5 +43,6 @@ async function requestAuth(flow: ModrinthAuthFlow) {
|
||||
defineExpose<SharedInstanceInviteHandler>({
|
||||
handleNotification,
|
||||
installFromInviteId,
|
||||
clearNotifications,
|
||||
})
|
||||
</script>
|
||||
|
||||
+1
@@ -26,4 +26,5 @@ export type SharedInstanceInvite = {
|
||||
export type SharedInstanceInviteHandler = {
|
||||
handleNotification(notification: AppNotification): Promise<boolean>
|
||||
installFromInviteId(inviteId: string): Promise<void>
|
||||
clearNotifications(): void
|
||||
}
|
||||
|
||||
+89
-23
@@ -6,11 +6,9 @@ import {
|
||||
injectPopupNotificationManager,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { type Ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { config } from '@/config'
|
||||
import { get_user } from '@/helpers/cache'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import {
|
||||
@@ -19,6 +17,7 @@ import {
|
||||
install_shared_instance,
|
||||
} from '@/helpers/install'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
import { parseSharedInstanceInviteNotification } from './shared-instance-invite-parser'
|
||||
@@ -28,9 +27,16 @@ type InstallModal = {
|
||||
show(
|
||||
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
|
||||
install: () => Promise<void>,
|
||||
creator?: SharedInstanceCreator,
|
||||
): void
|
||||
}
|
||||
|
||||
type SharedInstanceCreator = {
|
||||
id: string | null
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
}
|
||||
|
||||
type AccountRequiredModal = {
|
||||
show(event?: MouseEvent): Promise<boolean>
|
||||
}
|
||||
@@ -47,16 +53,22 @@ export function useSharedInstanceInviteHandler(
|
||||
const auth = injectAuth()
|
||||
const client = injectModrinthClient()
|
||||
const { handleError } = injectNotificationManager()
|
||||
const { addPopupNotification } = injectPopupNotificationManager()
|
||||
const { notifySharedInstanceConnectionError, notifySharedInstanceError } =
|
||||
useSharedInstanceErrors()
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
const queryClient = useQueryClient()
|
||||
const router = useRouter()
|
||||
const themeStore = useTheming()
|
||||
const displayedNotifications = new Set<string | number>()
|
||||
const displayedNotificationKeys = new Set<string>()
|
||||
const popupNotificationIds = new Set<string | number>()
|
||||
let notificationGeneration = 0
|
||||
let pendingAlreadyInstalled:
|
||||
| {
|
||||
instanceId: string
|
||||
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>
|
||||
install: () => Promise<void>
|
||||
creator?: SharedInstanceCreator
|
||||
onGoToInstance?: () => void | Promise<void>
|
||||
}
|
||||
| undefined
|
||||
@@ -72,10 +84,13 @@ export function useSharedInstanceInviteHandler(
|
||||
|
||||
async function resolveInvite(invite: SharedInstanceInvite) {
|
||||
const [invitedBy, sharedInstance] = await Promise.all([
|
||||
!invite.invitedByUsername && invite.invitedById
|
||||
(!invite.invitedByUsername || !invite.invitedByAvatarUrl) && invite.invitedById
|
||||
? get_user(invite.invitedById, 'bypass').catch(() => null)
|
||||
: null,
|
||||
client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => null),
|
||||
client.sharedinstances.instances_v1.get(invite.sharedInstanceId).catch(() => {
|
||||
notifySharedInstanceConnectionError()
|
||||
return null
|
||||
}),
|
||||
])
|
||||
|
||||
return {
|
||||
@@ -89,15 +104,17 @@ export function useSharedInstanceInviteHandler(
|
||||
function showInstall(
|
||||
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
|
||||
install: () => Promise<void>,
|
||||
creator?: SharedInstanceCreator,
|
||||
) {
|
||||
if (!installModal.value) throw new Error('Shared instance install modal is not available.')
|
||||
installModal.value.show(preview, install)
|
||||
installModal.value.show(preview, install, creator)
|
||||
}
|
||||
|
||||
async function showInstallOrAlreadyInstalled(
|
||||
sharedInstanceId: string,
|
||||
preview: Awaited<ReturnType<typeof install_get_shared_instance_preview>>,
|
||||
install: () => Promise<void>,
|
||||
creator?: SharedInstanceCreator,
|
||||
onGoToInstance?: () => void | Promise<void>,
|
||||
) {
|
||||
const existingInstance = (await list()).find(
|
||||
@@ -105,7 +122,7 @@ export function useSharedInstanceInviteHandler(
|
||||
)
|
||||
|
||||
if (!existingInstance || themeStore.getFeatureFlag('skip_non_essential_warnings')) {
|
||||
showInstall(preview, install)
|
||||
showInstall(preview, install, creator)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,6 +134,7 @@ export function useSharedInstanceInviteHandler(
|
||||
instanceId: existingInstance.id,
|
||||
preview,
|
||||
install,
|
||||
creator,
|
||||
onGoToInstance,
|
||||
}
|
||||
alreadyInstalledModal.value.show(existingInstance.name)
|
||||
@@ -145,7 +163,7 @@ export function useSharedInstanceInviteHandler(
|
||||
const pending = pendingAlreadyInstalled
|
||||
pendingAlreadyInstalled = undefined
|
||||
if (!pending) return
|
||||
showInstall(pending.preview, pending.install)
|
||||
showInstall(pending.preview, pending.install, pending.creator)
|
||||
}
|
||||
|
||||
async function acceptNotification(notification: AppNotification, invite: SharedInstanceInvite) {
|
||||
@@ -171,10 +189,17 @@ export function useSharedInstanceInviteHandler(
|
||||
await markNotificationRead(notification)
|
||||
await queryClient.invalidateQueries({ queryKey: ['instances'] })
|
||||
},
|
||||
invite.invitedByUsername
|
||||
? {
|
||||
id: invite.invitedById,
|
||||
username: invite.invitedByUsername,
|
||||
avatarUrl: invite.invitedByAvatarUrl,
|
||||
}
|
||||
: undefined,
|
||||
() => markNotificationRead(notification),
|
||||
)
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
notifySharedInstanceError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,9 +208,23 @@ export function useSharedInstanceInviteHandler(
|
||||
if (!parsedInvite) return false
|
||||
if (displayedNotifications.has(notification.id)) return true
|
||||
|
||||
const generation = notificationGeneration
|
||||
displayedNotifications.add(notification.id)
|
||||
const invite = await resolveInvite(parsedInvite)
|
||||
addPopupNotification({
|
||||
if (generation !== notificationGeneration) return true
|
||||
|
||||
const notificationKey = JSON.stringify([
|
||||
invite.invitedById ?? invite.invitedByUsername,
|
||||
invite.sharedInstanceName,
|
||||
invite.instanceIconUrl,
|
||||
])
|
||||
if (displayedNotificationKeys.has(notificationKey)) {
|
||||
await markNotificationRead(notification).catch((error) => handleError(toError(error)))
|
||||
return true
|
||||
}
|
||||
|
||||
displayedNotificationKeys.add(notificationKey)
|
||||
const popupNotification = popupNotificationManager.addPopupNotification({
|
||||
title: invite.sharedInstanceName,
|
||||
autoCloseMs: null,
|
||||
toast: {
|
||||
@@ -199,14 +238,26 @@ export function useSharedInstanceInviteHandler(
|
||||
markNotificationRead(notification).catch((error) => handleError(toError(error))),
|
||||
onOpenActor: () => {
|
||||
if (invite.invitedByUsername) {
|
||||
openUrl(`${config.siteUrl}/user/${encodeURIComponent(invite.invitedByUsername)}`)
|
||||
void router.push(`/user/${encodeURIComponent(invite.invitedByUsername)}`)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
popupNotificationIds.add(popupNotification.id)
|
||||
return true
|
||||
}
|
||||
|
||||
function clearNotifications() {
|
||||
notificationGeneration++
|
||||
for (const id of popupNotificationIds) {
|
||||
popupNotificationManager.removeNotification(id)
|
||||
}
|
||||
displayedNotifications.clear()
|
||||
displayedNotificationKeys.clear()
|
||||
popupNotificationIds.clear()
|
||||
pendingAlreadyInstalled = undefined
|
||||
}
|
||||
|
||||
async function requireAccount() {
|
||||
if (!auth.isReady?.value) {
|
||||
await new Promise<void>((resolve) => {
|
||||
@@ -226,25 +277,40 @@ export function useSharedInstanceInviteHandler(
|
||||
try {
|
||||
if (!(await requireAccount())) return
|
||||
const invite = await install_accept_shared_instance_invite(inviteId)
|
||||
await showInstallOrAlreadyInstalled(invite.sharedInstanceId, invite.preview, async () => {
|
||||
await install_shared_instance(
|
||||
invite.sharedInstanceId,
|
||||
invite.preview.name,
|
||||
invite.managerId,
|
||||
invite.serverManagerName,
|
||||
invite.serverManagerIconUrl,
|
||||
invite.instanceIconUrl,
|
||||
)
|
||||
await queryClient.invalidateQueries({ queryKey: ['instances'] })
|
||||
})
|
||||
const manager = invite.managerId
|
||||
? await get_user(invite.managerId, 'bypass').catch(() => null)
|
||||
: null
|
||||
await showInstallOrAlreadyInstalled(
|
||||
invite.sharedInstanceId,
|
||||
invite.preview,
|
||||
async () => {
|
||||
await install_shared_instance(
|
||||
invite.sharedInstanceId,
|
||||
invite.preview.name,
|
||||
invite.managerId,
|
||||
invite.serverManagerName,
|
||||
invite.serverManagerIconUrl,
|
||||
invite.instanceIconUrl,
|
||||
)
|
||||
await queryClient.invalidateQueries({ queryKey: ['instances'] })
|
||||
},
|
||||
manager
|
||||
? {
|
||||
id: manager.id,
|
||||
username: manager.username,
|
||||
avatarUrl: manager.avatar_url ?? null,
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
} catch (error) {
|
||||
handleError(toError(error))
|
||||
notifySharedInstanceError(error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleNotification,
|
||||
installFromInviteId,
|
||||
clearNotifications,
|
||||
handleAlreadyInstalledCancel,
|
||||
handleAlreadyInstalledGoToInstance,
|
||||
handleAlreadyInstalledInstallAnyway,
|
||||
|
||||
@@ -22,11 +22,9 @@
|
||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||
<section v-if="mode === 'edit' && canEditTextureAndModel">
|
||||
<h2 class="text-base font-semibold mb-2">{{ formatMessage(messages.textureSection) }}</h2>
|
||||
<ButtonStyled>
|
||||
<button class="!shadow-none" @click="openTextureFileBrowser">
|
||||
<UploadIcon /> {{ formatMessage(messages.replaceTextureButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button @click="openTextureFileBrowser">
|
||||
<UploadIcon /> {{ formatMessage(messages.replaceTextureButton) }}
|
||||
</Button>
|
||||
<input
|
||||
ref="textureFileInput"
|
||||
type="file"
|
||||
@@ -112,19 +110,21 @@
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button :disabled="isSaving" @click="hide">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ formatMessage(mode === 'new' ? messages.addSkinButton : messages.saveSkinButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" :disabled="isSaving" @click="hide">
|
||||
<XIcon />{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip="saveTooltip"
|
||||
type="colored"
|
||||
color="brand"
|
||||
:disabled="disableSave || isSaving"
|
||||
@click="save"
|
||||
>
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ formatMessage(mode === 'new' ? messages.addSkinButton : messages.saveSkinButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
@@ -133,7 +133,7 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckIcon, SaveIcon, SpinnerIcon, UploadIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
commonMessages,
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon, UnknownIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Accordion,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
IconButton,
|
||||
SkinButton,
|
||||
SkinLikeTextButton,
|
||||
useScrollViewport,
|
||||
@@ -463,25 +464,26 @@ defineExpose({ getAddSkinButtonElement })
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template v-if="!readOnly" #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
:aria-label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</Button>
|
||||
<IconButton
|
||||
v-show="!skin.is_equipped"
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
type="colored"
|
||||
color="red"
|
||||
:label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
@@ -503,25 +505,26 @@ defineExpose({ getAddSkinButtonElement })
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template v-if="!readOnly" #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-show="!skin.is_equipped" circular color="red">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
:aria-label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</Button>
|
||||
<IconButton
|
||||
v-show="!skin.is_equipped"
|
||||
v-tooltip="formatMessage(messages.deleteSkinButton)"
|
||||
type="colored"
|
||||
color="red"
|
||||
:label="formatMessage(messages.deleteSkinButton)"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
@click.stop="emit('delete', skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
@@ -545,15 +548,15 @@ defineExpose({ getAddSkinButtonElement })
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button
|
||||
type="colored"
|
||||
color="brand"
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(event: MouseEvent) => emit('edit', skin, event)"
|
||||
>
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</Button>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@ import {
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
TeleportOverflowMenu,
|
||||
useFormatDateTime,
|
||||
useRelativeTime,
|
||||
useVIntl,
|
||||
@@ -185,54 +185,53 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||
<ButtonStyled v-if="playing && !loading" color="red">
|
||||
<button @click="stop">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
instance.quarantined
|
||||
? 'This instance has been locked'
|
||||
: playing
|
||||
? 'Instance is already open'
|
||||
: null
|
||||
"
|
||||
:disabled="instance.quarantined || playing || loading"
|
||||
@click="play"
|
||||
>
|
||||
<SpinnerIcon v-if="loading" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instance.id,
|
||||
action: () => router.push(encodeURI(`/instance/${instance.id}`)),
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
action: () => showInstanceInFolder(instance.id),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
View instance
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
<Button v-if="playing && !loading" type="colored" color="red" @click="stop">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
v-tooltip="
|
||||
instance.quarantined
|
||||
? 'This instance has been locked'
|
||||
: playing
|
||||
? 'Instance is already open'
|
||||
: null
|
||||
"
|
||||
:disabled="instance.quarantined || playing || loading"
|
||||
@click="play"
|
||||
>
|
||||
<SpinnerIcon v-if="loading" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</Button>
|
||||
<TeleportOverflowMenu
|
||||
type="quiet"
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'open-instance',
|
||||
label: 'View instance',
|
||||
shown: !!instance.id,
|
||||
action: () => router.push(encodeURI(`/instance/${instance.id}`)),
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: formatMessage(commonMessages.openFolderButton),
|
||||
action: () => showInstanceInFolder(instance.id),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
View instance
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { LoaderCircleIcon } from '@modrinth/assets'
|
||||
import type { GameVersion } from '@modrinth/ui'
|
||||
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||
import { GAME_MODES, injectNotificationManager } from '@modrinth/ui'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -268,13 +268,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||
Jump back in
|
||||
</HeadingLink>
|
||||
<span
|
||||
v-else
|
||||
class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold"
|
||||
>
|
||||
<span class="flex mt-1 mb-3 leading-none items-center gap-1 text-primary text-lg font-bold">
|
||||
Jump back in
|
||||
</span>
|
||||
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||
|
||||
@@ -21,13 +21,13 @@ import {
|
||||
import type { MessageDescriptor } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
TagItem,
|
||||
TeleportOverflowMenu,
|
||||
useFormatDateTime,
|
||||
useFormatNumber,
|
||||
useRelativeTime,
|
||||
@@ -412,177 +412,184 @@ const messages = defineMessages({
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||
<ButtonStyled
|
||||
<Button
|
||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||
type="colored"
|
||||
color="red"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<button @click="emit('stop')">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
quarantined
|
||||
? 'This instance has been locked'
|
||||
: world.type === 'server'
|
||||
? !supportsServerQuickPlay
|
||||
? formatMessage(messages.noServerQuickPlay)
|
||||
: playingOtherWorld
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: !serverStatus
|
||||
? formatMessage(messages.noContact)
|
||||
: serverIncompatible
|
||||
? formatMessage(messages.incompatibleServer)
|
||||
: null
|
||||
: !supportsWorldQuickPlay
|
||||
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||
: playingOtherWorld || locked
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: null
|
||||
"
|
||||
:disabled="
|
||||
quarantined ||
|
||||
playingOtherWorld ||
|
||||
startingInstance ||
|
||||
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||
"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
shown: !!instanceId,
|
||||
disabled: playingInstance || quarantined,
|
||||
action: () => emit('play-instance'),
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
v-tooltip="
|
||||
quarantined
|
||||
? 'This instance has been locked'
|
||||
: world.type === 'server'
|
||||
? !supportsServerQuickPlay
|
||||
? formatMessage(messages.noServerQuickPlay)
|
||||
: playingOtherWorld
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: !serverStatus
|
||||
? formatMessage(messages.noContact)
|
||||
: serverIncompatible
|
||||
? formatMessage(messages.incompatibleServer)
|
||||
: null
|
||||
: !supportsWorldQuickPlay
|
||||
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||
: playingOtherWorld || locked
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: null
|
||||
"
|
||||
:disabled="
|
||||
quarantined ||
|
||||
playingOtherWorld ||
|
||||
startingInstance ||
|
||||
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||
"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</Button>
|
||||
<TeleportOverflowMenu
|
||||
type="quiet"
|
||||
label="More options"
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
label: formatMessage(messages.playInstance),
|
||||
shown: !!instanceId,
|
||||
disabled: playingInstance || quarantined,
|
||||
action: () => emit('play-instance'),
|
||||
},
|
||||
{
|
||||
id: 'open-instance',
|
||||
label: formatMessage(messages.viewInstance),
|
||||
shown: !!instanceId,
|
||||
action: () => router.push(`/instance/${encodeURIComponent(instanceId)}`),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
label: formatMessage(commonMessages.refreshButton),
|
||||
shown: world.type === 'server',
|
||||
action: () => emit('refresh'),
|
||||
},
|
||||
{
|
||||
id: 'copy-address',
|
||||
label: formatMessage(messages.copyAddress),
|
||||
shown: world.type === 'server',
|
||||
action: () => copyToClipboard((world as ServerWorld).address),
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
label: formatMessage(commonMessages.editButton),
|
||||
action: () => emit('edit'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
label: formatMessage(commonMessages.openFolderButton),
|
||||
shown: world.type === 'singleplayer',
|
||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
shown: !!instanceId,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
label: formatMessage(messages.dontShowOnHome),
|
||||
shown: !!instanceId,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instanceId,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
).then(() => {
|
||||
emit('update')
|
||||
})
|
||||
},
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instanceId,
|
||||
action: () => router.push(`/instance/${encodeURIComponent(instanceId)}`),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
shown: world.type === 'server',
|
||||
action: () => emit('refresh'),
|
||||
},
|
||||
{
|
||||
id: 'copy-address',
|
||||
shown: world.type === 'server',
|
||||
action: () => copyToClipboard((world as ServerWorld).address),
|
||||
},
|
||||
{
|
||||
id: 'edit',
|
||||
action: () => emit('edit'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'open-folder',
|
||||
shown: world.type === 'singleplayer',
|
||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !!instanceId,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
shown: !!instanceId,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instanceId,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
).then(() => {
|
||||
emit('update')
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
shown: !!shortcutInstanceId && !quarantined,
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !instanceId,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
action: () => emit('delete'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #play-instance>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playInstance) }}
|
||||
</template>
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.viewInstance) }}
|
||||
</template>
|
||||
<template #edit>
|
||||
<EditIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
<template #copy-address>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.copyAddress) }}
|
||||
</template>
|
||||
<template #refresh>
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #create-shortcut>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createShortcut) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{
|
||||
formatMessage(
|
||||
world.type === 'server'
|
||||
? commonMessages.removeButton
|
||||
: commonMessages.deleteLabel,
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
},
|
||||
{
|
||||
id: 'create-shortcut',
|
||||
label: formatMessage(messages.createShortcut),
|
||||
shown: !!shortcutInstanceId && !quarantined,
|
||||
action: () => createShortcut(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
shown: !instanceId,
|
||||
},
|
||||
{
|
||||
id: 'delete',
|
||||
label: formatMessage(
|
||||
world.type === 'server' ? commonMessages.removeButton : commonMessages.deleteLabel,
|
||||
),
|
||||
tone: 'red',
|
||||
action: () => emit('delete'),
|
||||
shown: !instanceId,
|
||||
disabled: locked || managed,
|
||||
tooltip: locked
|
||||
? formatMessage(messages.worldInUse)
|
||||
: managed
|
||||
? formatMessage(messages.linkedServer)
|
||||
: undefined,
|
||||
},
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #play-instance>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playInstance) }}
|
||||
</template>
|
||||
<template #open-instance>
|
||||
<EyeIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.viewInstance) }}
|
||||
</template>
|
||||
<template #edit>
|
||||
<EditIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</template>
|
||||
<template #open-folder>
|
||||
<FolderOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||
</template>
|
||||
<template #copy-address>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.copyAddress) }}
|
||||
</template>
|
||||
<template #refresh>
|
||||
<UpdatedIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #create-shortcut>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createShortcut) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{
|
||||
formatMessage(
|
||||
world.type === 'server' ? commonMessages.removeButton : commonMessages.deleteLabel,
|
||||
)
|
||||
}}
|
||||
</template>
|
||||
</TeleportOverflowMenu>
|
||||
</div>
|
||||
</div>
|
||||
</SmartClickable>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
@@ -96,24 +96,18 @@ defineExpose({ show, hide })
|
||||
/>
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="!address" @click="addServer(false)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!address" @click="addServer(true)">
|
||||
<PlayIcon />
|
||||
{{ formatMessage(messages.addAndPlay) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button :disabled="!address" @click="addServer(false)">
|
||||
<PlusIcon />
|
||||
{{ formatMessage(messages.addServer) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" :disabled="!address" @click="addServer(true)">
|
||||
<PlayIcon />
|
||||
{{ formatMessage(messages.addAndPlay) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { TrashIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Admonition,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
NewModal,
|
||||
@@ -106,18 +106,19 @@ defineExpose({ show, hide })
|
||||
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="red">
|
||||
<button :disabled="!isServer && !isSingleplayer" @click="confirm">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(actionMessage) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="hide">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
type="colored"
|
||||
color="red"
|
||||
:disabled="!isServer && !isSingleplayer"
|
||||
@click="confirm"
|
||||
>
|
||||
<TrashIcon />
|
||||
{{ formatMessage(actionMessage) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessage,
|
||||
injectNotificationManager,
|
||||
@@ -105,18 +105,14 @@ const titleMessage = defineMessage({
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
<template #actions>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<ButtonStyled type="outlined">
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!address" @click="saveServer">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="outlined" @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
<Button type="colored" color="brand" :disabled="!address" @click="saveServer">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</NewModal>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
injectNotificationManager,
|
||||
@@ -113,24 +113,18 @@ const messages = defineMessages({
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="saveWorld">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
||||
<UndoIcon />
|
||||
{{ formatMessage(messages.resetIcon) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button type="colored" color="brand" @click="saveWorld">
|
||||
<SaveIcon />
|
||||
{{ formatMessage(commonMessages.saveChangesButton) }}
|
||||
</Button>
|
||||
<Button :disabled="removeIcon || !icon" @click="removeIcon = true">
|
||||
<UndoIcon />
|
||||
{{ formatMessage(messages.resetIcon) }}
|
||||
</Button>
|
||||
<Button @click="hide()">
|
||||
<XIcon />
|
||||
{{ formatMessage(commonMessages.cancelButton) }}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type InstallProgress,
|
||||
} from '@/helpers/install'
|
||||
import { get_many as getInstances } from '@/helpers/instance'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const messages = defineMessages({
|
||||
installs: {
|
||||
@@ -221,13 +222,6 @@ const failureSummaryMessages = defineMessages({
|
||||
})
|
||||
|
||||
const visibleJobStatuses = new Set<InstallJobStatus>(['queued', 'running', 'failed', 'interrupted'])
|
||||
const copyDetailsStallMs = 30_000
|
||||
|
||||
interface ProgressSnapshot {
|
||||
signature: string
|
||||
changedAt: number
|
||||
timeout: number | null
|
||||
}
|
||||
|
||||
function getDisplayIconUrl(icon: string | null | undefined): string | null {
|
||||
if (!icon) return null
|
||||
@@ -241,6 +235,7 @@ export async function useInstallJobNotifications(opts: {
|
||||
onChange: () => void
|
||||
}) {
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const jobs = ref<InstallJobSnapshot[]>([])
|
||||
const iconUrls = ref<Record<string, string | null>>({})
|
||||
const instanceNames = ref<Record<string, string>>({})
|
||||
@@ -250,7 +245,6 @@ export async function useInstallJobNotifications(opts: {
|
||||
let metadataRequest = 0
|
||||
let nextJobOrder = 0
|
||||
const copiedResetTimeouts = new Map<string, number>()
|
||||
const progressSnapshots = new Map<string, ProgressSnapshot>()
|
||||
|
||||
function getTitle(job: InstallJobSnapshot): string {
|
||||
if (job.display?.title) return job.display.title
|
||||
@@ -421,116 +415,14 @@ export async function useInstallJobNotifications(opts: {
|
||||
return job.status === 'failed' || job.status === 'interrupted'
|
||||
}
|
||||
|
||||
function canShowStalledProgressDetails(job: InstallJobSnapshot): boolean {
|
||||
return (
|
||||
job.status === 'running' &&
|
||||
job.phase !== 'preparing_instance' &&
|
||||
job.phase !== 'finalizing' &&
|
||||
job.phase !== 'rolling_back'
|
||||
)
|
||||
}
|
||||
|
||||
function getJobSortRank(job: InstallJobSnapshot): number {
|
||||
if (isTerminalJob(job)) return 0
|
||||
if (job.status === 'queued' || job.phase === 'preparing_instance') return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
function progressSignature(job: InstallJobSnapshot): string {
|
||||
const progress = job.progress
|
||||
const secondary = progress?.secondary
|
||||
return [
|
||||
job.status,
|
||||
job.phase,
|
||||
JSON.stringify(job.details),
|
||||
progress?.current ?? '',
|
||||
progress?.total ?? '',
|
||||
secondary?.current ?? '',
|
||||
secondary?.total ?? '',
|
||||
].join(':')
|
||||
}
|
||||
|
||||
function clearCopied(jobId: string) {
|
||||
if (!copiedJobIds.value.has(jobId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = copiedResetTimeouts.get(jobId)
|
||||
if (timeout != null) {
|
||||
window.clearTimeout(timeout)
|
||||
copiedResetTimeouts.delete(jobId)
|
||||
}
|
||||
|
||||
const nextCopiedJobIds = new Set(copiedJobIds.value)
|
||||
nextCopiedJobIds.delete(jobId)
|
||||
copiedJobIds.value = nextCopiedJobIds
|
||||
}
|
||||
|
||||
function clearProgressSnapshot(jobId: string) {
|
||||
const snapshot = progressSnapshots.get(jobId)
|
||||
if (snapshot?.timeout != null) {
|
||||
window.clearTimeout(snapshot.timeout)
|
||||
}
|
||||
progressSnapshots.delete(jobId)
|
||||
}
|
||||
|
||||
function scheduleStaleProgressRefresh(jobId: string) {
|
||||
const snapshot = progressSnapshots.get(jobId)
|
||||
if (!snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.timeout = window.setTimeout(() => {
|
||||
const snapshot = progressSnapshots.get(jobId)
|
||||
if (!snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
snapshot.timeout = null
|
||||
opts.onChange()
|
||||
}, copyDetailsStallMs)
|
||||
}
|
||||
|
||||
function syncProgressSnapshots(nextJobs: InstallJobSnapshot[]) {
|
||||
const trackedJobIds = new Set<string>()
|
||||
const now = Date.now()
|
||||
|
||||
for (const job of nextJobs) {
|
||||
if (!canShowStalledProgressDetails(job)) {
|
||||
continue
|
||||
}
|
||||
|
||||
trackedJobIds.add(job.job_id)
|
||||
const signature = progressSignature(job)
|
||||
const snapshot = progressSnapshots.get(job.job_id)
|
||||
if (snapshot?.signature === signature) {
|
||||
continue
|
||||
}
|
||||
|
||||
clearProgressSnapshot(job.job_id)
|
||||
clearCopied(job.job_id)
|
||||
progressSnapshots.set(job.job_id, {
|
||||
signature,
|
||||
changedAt: now,
|
||||
timeout: null,
|
||||
})
|
||||
scheduleStaleProgressRefresh(job.job_id)
|
||||
}
|
||||
|
||||
for (const jobId of progressSnapshots.keys()) {
|
||||
if (!trackedJobIds.has(jobId)) {
|
||||
clearProgressSnapshot(jobId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasStalledProgress(job: InstallJobSnapshot): boolean {
|
||||
const snapshot = progressSnapshots.get(job.job_id)
|
||||
return !!snapshot && Date.now() - snapshot.changedAt >= copyDetailsStallMs
|
||||
}
|
||||
|
||||
function shouldShowCopyDetails(job: InstallJobSnapshot): boolean {
|
||||
return isTerminalJob(job) || (canShowStalledProgressDetails(job) && hasStalledProgress(job))
|
||||
return isTerminalJob(job) || themeStore.getFeatureFlag('always_show_copy_details')
|
||||
}
|
||||
|
||||
function isCopied(job: InstallJobSnapshot): boolean {
|
||||
@@ -607,6 +499,16 @@ export async function useInstallJobNotifications(opts: {
|
||||
return buttons
|
||||
}
|
||||
|
||||
function getDismissHandler(job: InstallJobSnapshot): (() => Promise<void>) | undefined {
|
||||
if (isTerminalJob(job)) {
|
||||
return async () => {
|
||||
await install_job_dismiss(job.job_id).catch(opts.handleError)
|
||||
await refresh()
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function setJobs(nextJobs: InstallJobSnapshot[]) {
|
||||
for (const job of nextJobs) {
|
||||
if (!jobOrder.has(job.job_id)) {
|
||||
@@ -615,7 +517,6 @@ export async function useInstallJobNotifications(opts: {
|
||||
}
|
||||
|
||||
const visibleJobs = nextJobs.filter((job) => visibleJobStatuses.has(job.status))
|
||||
syncProgressSnapshots(visibleJobs)
|
||||
|
||||
jobs.value = visibleJobs.sort(
|
||||
(a, b) =>
|
||||
@@ -642,12 +543,8 @@ export async function useInstallJobNotifications(opts: {
|
||||
progressCurrent: isTerminalJob(job) ? undefined : progress?.current,
|
||||
progressTotal: isTerminalJob(job) ? undefined : progress?.total,
|
||||
buttons: getButtons(job),
|
||||
onDismiss: isTerminalJob(job)
|
||||
? async () => {
|
||||
await install_job_dismiss(job.job_id).catch(opts.handleError)
|
||||
await refresh()
|
||||
}
|
||||
: undefined,
|
||||
dismissible: isTerminalJob(job),
|
||||
onDismiss: getDismissHandler(job),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -730,8 +627,8 @@ export async function useInstallJobNotifications(opts: {
|
||||
void refreshMetadata()
|
||||
}
|
||||
|
||||
await refresh(false)
|
||||
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
|
||||
await refresh(false)
|
||||
|
||||
return {
|
||||
active: computed(() => jobs.value.length > 0),
|
||||
@@ -743,9 +640,6 @@ export async function useInstallJobNotifications(opts: {
|
||||
for (const timeout of copiedResetTimeouts.values()) {
|
||||
window.clearTimeout(timeout)
|
||||
}
|
||||
for (const jobId of progressSnapshots.keys()) {
|
||||
clearProgressSnapshot(jobId)
|
||||
}
|
||||
unlisten()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ export async function hide_ads_window(reset) {
|
||||
return await invoke('plugin:ads|hide_ads_window', { reset })
|
||||
}
|
||||
|
||||
export async function get_ads_consent_required() {
|
||||
return await invoke('plugin:ads|get_ads_consent_required')
|
||||
export async function should_show_ads_consent_popup() {
|
||||
return await invoke('plugin:ads|should_show_ads_consent_popup')
|
||||
}
|
||||
|
||||
export async function perform_ads_consent_action(action) {
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface SharedInstanceUpdateDiff {
|
||||
}
|
||||
|
||||
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
|
||||
export const SHARED_INSTANCES_API_ERROR_CODE = 'shared_instances_api_error'
|
||||
|
||||
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked' | 'quarantined'
|
||||
|
||||
@@ -113,6 +114,10 @@ export function isSharedInstanceUnavailableError(error: unknown) {
|
||||
return getSharedInstanceUnavailableReason(error) !== null
|
||||
}
|
||||
|
||||
export function isSharedInstancesApiError(error: unknown) {
|
||||
return isRecord(error) && error.code === SHARED_INSTANCES_API_ERROR_CODE
|
||||
}
|
||||
|
||||
export function getSharedInstanceUnavailableReason(
|
||||
error: unknown,
|
||||
): SharedInstanceUnavailableReason | null {
|
||||
|
||||
@@ -365,6 +365,13 @@ export interface SharedInstanceInviteLink {
|
||||
maxUses: number
|
||||
}
|
||||
|
||||
export interface SharedInstanceInvite {
|
||||
id: string
|
||||
expiration: string
|
||||
maxUses: number
|
||||
uses: number
|
||||
}
|
||||
|
||||
export async function can_current_user_use_shared_instances(): Promise<boolean> {
|
||||
return await invoke('plugin:instance|instance_share_can_current_user_use')
|
||||
}
|
||||
@@ -394,6 +401,19 @@ export async function create_shared_instance_invite_link(
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_shared_instance_invites(
|
||||
instanceId: string,
|
||||
): Promise<SharedInstanceInvite[]> {
|
||||
return await invoke('plugin:instance|instance_share_get_invites', { instanceId })
|
||||
}
|
||||
|
||||
export async function revoke_shared_instance_invite(
|
||||
instanceId: string,
|
||||
inviteId: string,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:instance|instance_share_revoke_invite', { instanceId, inviteId })
|
||||
}
|
||||
|
||||
export async function remove_shared_instance_users(
|
||||
instanceId: string,
|
||||
userIds: string[],
|
||||
|
||||
@@ -37,7 +37,7 @@ export type AppSettings = {
|
||||
|
||||
theme: ColorTheme
|
||||
locale: string
|
||||
default_page: 'home' | 'library'
|
||||
default_page: 'Home' | 'Library'
|
||||
collapsed_navigation: boolean
|
||||
hide_nametag_skins_page: boolean
|
||||
advanced_rendering: boolean
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/ui'
|
||||
|
||||
import { getErrorMessage, type SharedInstanceUnavailableReason } from '@/helpers/install'
|
||||
import {
|
||||
getErrorMessage,
|
||||
isSharedInstancesApiError,
|
||||
type SharedInstanceUnavailableReason,
|
||||
} from '@/helpers/install'
|
||||
|
||||
export const sharedInstanceErrorMessages = defineMessages({
|
||||
unavailableTitle: {
|
||||
@@ -39,6 +43,14 @@ export const sharedInstanceErrorMessages = defineMessages({
|
||||
id: 'instance.shared-instance.error.title',
|
||||
defaultMessage: 'Something has gone wrong',
|
||||
},
|
||||
networkErrorTitle: {
|
||||
id: 'instance.shared-instance.network-error.title',
|
||||
defaultMessage: 'Network error',
|
||||
},
|
||||
networkErrorText: {
|
||||
id: 'instance.shared-instance.network-error.text',
|
||||
defaultMessage: 'Unable to connect to shared instances API',
|
||||
},
|
||||
})
|
||||
|
||||
export function sharedInstanceUnavailableTextMessage(
|
||||
@@ -82,7 +94,20 @@ export function useSharedInstanceErrors() {
|
||||
})
|
||||
}
|
||||
|
||||
function notifySharedInstanceConnectionError() {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(sharedInstanceErrorMessages.networkErrorTitle),
|
||||
text: formatMessage(sharedInstanceErrorMessages.networkErrorText),
|
||||
})
|
||||
}
|
||||
|
||||
function notifySharedInstanceError(error: unknown) {
|
||||
if (isSharedInstancesApiError(error)) {
|
||||
notifySharedInstanceConnectionError()
|
||||
return
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: formatMessage(sharedInstanceErrorMessages.errorTitle),
|
||||
@@ -92,6 +117,7 @@ export function useSharedInstanceErrors() {
|
||||
|
||||
return {
|
||||
formatSharedInstanceUnavailable,
|
||||
notifySharedInstanceConnectionError,
|
||||
notifySharedInstanceError,
|
||||
notifySharedInstanceUnavailable,
|
||||
}
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ type AppSettings = {
|
||||
max_concurrent_downloads: number
|
||||
max_concurrent_writes: number
|
||||
|
||||
theme: 'dark' | 'light' | 'oled'
|
||||
theme: 'dark' | 'light' | 'oled' | 'retro' | 'system'
|
||||
default_page: 'Home' | 'Library'
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
|
||||
@@ -1,11 +1,80 @@
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export type SearchUser = {
|
||||
id: string
|
||||
username: string
|
||||
avatar_url: string | null
|
||||
// Converts user profile links from rendered Markdown/any dynamic content into app routes.
|
||||
export function parse_modrinth_user_link(href: string): string | null {
|
||||
try {
|
||||
const url = new URL(href)
|
||||
if (url.hostname !== 'modrinth.com' && url.hostname !== 'www.modrinth.com') return null
|
||||
|
||||
const segments = url.pathname.split('/').filter(Boolean)
|
||||
if (segments[0]?.toLowerCase() !== 'user' || !segments[1] || segments.length > 3) return null
|
||||
|
||||
const path = `/user/${encodeURIComponent(decodeURIComponent(segments[1]))}`
|
||||
return segments[2] ? `${path}/${encodeURIComponent(decodeURIComponent(segments[2]))}` : path
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function search_user(query: string): Promise<SearchUser[]> {
|
||||
return await invoke<SearchUser[]>('plugin:users|search_user', { query })
|
||||
export async function search_user(query: string): Promise<Labrinth.Users.v3.SearchUser[]> {
|
||||
return await invoke<Labrinth.Users.v3.SearchUser[]>('plugin:users|search_user', { query })
|
||||
}
|
||||
|
||||
export async function get_user_profile(userId: string): Promise<Labrinth.Users.v3.User> {
|
||||
return await invoke<Labrinth.Users.v3.User>('plugin:users|get_user_profile', { userId })
|
||||
}
|
||||
|
||||
export async function get_user_projects(userId: string): Promise<Labrinth.Projects.v2.Project[]> {
|
||||
return await invoke<Labrinth.Projects.v2.Project[]>('plugin:users|get_user_projects', {
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_user_organizations(
|
||||
userId: string,
|
||||
): Promise<Labrinth.Organizations.v3.Organization[]> {
|
||||
return await invoke<Labrinth.Organizations.v3.Organization[]>(
|
||||
'plugin:users|get_user_organizations',
|
||||
{ userId },
|
||||
)
|
||||
}
|
||||
|
||||
export async function get_user_collections(
|
||||
userId: string,
|
||||
): Promise<Labrinth.Collections.Collection[]> {
|
||||
return await invoke<Labrinth.Collections.Collection[]>('plugin:users|get_user_collections', {
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function patch_user(
|
||||
userId: string,
|
||||
patch: Partial<Pick<Labrinth.Users.v2.User, 'badges' | 'bio' | 'role' | 'username'>>,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:users|patch_user', { userId, patch })
|
||||
}
|
||||
|
||||
export async function change_user_avatar(
|
||||
userId: string,
|
||||
image: Uint8Array,
|
||||
extension: string,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:users|change_user_avatar', { userId, image, extension })
|
||||
}
|
||||
|
||||
export async function delete_user_avatar(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|delete_user_avatar', { userId })
|
||||
}
|
||||
|
||||
export async function block_user(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|block_user', { userId })
|
||||
}
|
||||
|
||||
export async function unblock_user(userId: string): Promise<void> {
|
||||
await invoke('plugin:users|unblock_user', { userId })
|
||||
}
|
||||
|
||||
export async function get_blocked_users(): Promise<Labrinth.BlockedUsers.v3.BlockedUserId[]> {
|
||||
return await invoke<Labrinth.BlockedUsers.v3.BlockedUserId[]>('plugin:users|get_blocked_users')
|
||||
}
|
||||
|
||||
@@ -125,6 +125,15 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "عرض التسجيل"
|
||||
},
|
||||
"app.ads-consent.accept": {
|
||||
"message": "قبول الكل"
|
||||
},
|
||||
"app.ads-consent.manage": {
|
||||
"message": "إدارة التفضيلات"
|
||||
},
|
||||
"app.ads-consent.reject": {
|
||||
"message": "رفض الكل"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "يتيح العرض المتقدم لتأثيرات مثل التشويش الذي قد تسبب مشكلات في الأداء دون استخدام العرض المسرع بالأجهزة."
|
||||
},
|
||||
@@ -173,9 +182,6 @@
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "الزخارف التقليدية"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "حدد خياراً"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "يعرض مقدار الوقت الذي قضيته في لعب إحدى المهام."
|
||||
},
|
||||
@@ -227,9 +233,6 @@
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "العودة للنموذج"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "استكشف محتوى"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "استكشف خوادم"
|
||||
},
|
||||
@@ -428,24 +431,33 @@
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "نزل للعب"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "تثبيت على أي حال"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "تثبيت"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# تعديل} other {# تعديلات}}"
|
||||
},
|
||||
"app.modal.install-to-play.reviewed-files": {
|
||||
"message": "لا يتم مراجعة الملف إلا إذا تم نشره على Modrinth، بغض النظر عن صيغته (بما في ذلك .mrpack)."
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "النماذج المشتركة"
|
||||
},
|
||||
"app.modal.install-to-play.unknown-files-warning": {
|
||||
"message": "تحذير ملفات غير معروفة"
|
||||
},
|
||||
"app.modal.install-to-play.unrecognized-files": {
|
||||
"message": "ملفات غير متعرّف عليها"
|
||||
},
|
||||
"app.modal.install-to-play.view-contents": {
|
||||
"message": "عرض المحتويات"
|
||||
},
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "حدث للعب"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "هناك تحديث مطلوب للعب بـ {name}. الرجاء التحديث إلى أحدث اصدار لتشغيل اللعبة."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "هذا المشروع مُثبَّت بالفعل"
|
||||
},
|
||||
@@ -641,6 +653,12 @@
|
||||
"app.skins.title": {
|
||||
"message": "اختيار المظهر"
|
||||
},
|
||||
"app.skins.toggle-ears-features-off": {
|
||||
"message": "إيقاف"
|
||||
},
|
||||
"app.skins.toggle-ears-features-on": {
|
||||
"message": "تشغيل"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "انتهى تنزيل تطبيق Modrinth v{version}. أعد التحميل للتحديث الآن، أو تلقائيًا عند إغلاق تطبيق Modrinth."
|
||||
},
|
||||
@@ -752,6 +770,24 @@
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>سجّل الدخول إلى حساب Modrinth</link> لإضافة الأصدقاء ومعرفة ما الذي يلعبونه!"
|
||||
},
|
||||
"instance.action.create-shortcut": {
|
||||
"message": "إنشاء اختصار"
|
||||
},
|
||||
"instance.action.more-actions": {
|
||||
"message": "مزيد من الخيارات"
|
||||
},
|
||||
"instance.action.open-folder": {
|
||||
"message": "فتح المجلد"
|
||||
},
|
||||
"instance.action.repair": {
|
||||
"message": "إصلاح"
|
||||
},
|
||||
"instance.action.starting": {
|
||||
"message": "قيد التشغيل..."
|
||||
},
|
||||
"instance.action.stopping": {
|
||||
"message": "قيد الإيقاف..."
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "إضافة واللعب"
|
||||
},
|
||||
@@ -794,6 +830,9 @@
|
||||
"instance.files.save-as": {
|
||||
"message": "حفظ ك..."
|
||||
},
|
||||
"instance.playtime.never-played": {
|
||||
"message": "لم تُلعب من قبل"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "العنوان"
|
||||
},
|
||||
|
||||
@@ -113,9 +113,6 @@
|
||||
"app.appearance-settings.native-decorations.title": {
|
||||
"message": "Systémové dekorace"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Vyberte možnost"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Zobrazuje, kolik času jsi strávil hraním instance."
|
||||
},
|
||||
@@ -167,9 +164,6 @@
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Zpět k instanci"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Objevit obsah"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Objevit servery"
|
||||
},
|
||||
@@ -380,9 +374,6 @@
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Aktualizuj pro hraní"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "Pro hraní {name} je vyžadována aktualizace. Prosím aktualizuj na nejnovější verzi, abys mohl hru spustit."
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Tento projekt je již nainstalován"
|
||||
},
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
"app.action-bar.hide-more-running-instances": {
|
||||
"message": "Gem flere kørende instances"
|
||||
},
|
||||
"app.action-bar.install.copied-details": {
|
||||
"message": "Kopiere"
|
||||
},
|
||||
"app.action-bar.install.copy-details": {
|
||||
"message": "Kopier detaljer"
|
||||
},
|
||||
"app.action-bar.install.dismiss": {
|
||||
"message": "Tilsidesæt"
|
||||
},
|
||||
@@ -20,9 +26,69 @@
|
||||
"app.action-bar.install.retry": {
|
||||
"message": "Prøv igen"
|
||||
},
|
||||
"app.action-bar.install.summary.app-closing": {
|
||||
"message": "Annulleret på grund af appen blev lukket"
|
||||
},
|
||||
"app.action-bar.install.summary.bad-modpack-file": {
|
||||
"message": "Kunne ikke indlæse modpack"
|
||||
},
|
||||
"app.action-bar.install.summary.canceled": {
|
||||
"message": "Annulleret"
|
||||
},
|
||||
"app.action-bar.install.summary.content-download-failed": {
|
||||
"message": "Kunne ikke download filer"
|
||||
},
|
||||
"app.action-bar.install.summary.corrupt-download": {
|
||||
"message": "Downloaded filer er korrupt"
|
||||
},
|
||||
"app.action-bar.install.summary.could-not-save-files": {
|
||||
"message": "Kunne ikke gemme filer"
|
||||
},
|
||||
"app.action-bar.install.summary.download-failed": {
|
||||
"message": "Download kunne ikke blive fuldført"
|
||||
},
|
||||
"app.action-bar.install.summary.instance-not-found": {
|
||||
"message": "Instance kunne ikke blive fundet"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-file-path": {
|
||||
"message": "Fil sti er ugyldig"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack": {
|
||||
"message": "Modpack data ugyldig"
|
||||
},
|
||||
"app.action-bar.install.summary.invalid-modpack-files": {
|
||||
"message": "Modpack har ugyldig metadata"
|
||||
},
|
||||
"app.action-bar.install.summary.java-setup-failed": {
|
||||
"message": "Java setup kunne ikke færdiggøres"
|
||||
},
|
||||
"app.action-bar.install.summary.loader-setup-failed": {
|
||||
"message": "Loader setup fejlede"
|
||||
},
|
||||
"app.action-bar.install.summary.local-data-error": {
|
||||
"message": "Kunne ikke opdatere lokal data"
|
||||
},
|
||||
"app.action-bar.install.summary.minecraft-setup-failed": {
|
||||
"message": "Minecraft setup fejlede"
|
||||
},
|
||||
"app.action-bar.install.summary.modrinth-unreachable": {
|
||||
"message": "Kunne ikke få forbindelse til Modrinth"
|
||||
},
|
||||
"app.action-bar.install.summary.no-write-permission": {
|
||||
"message": "Ingen adgang til at skrive"
|
||||
},
|
||||
"app.action-bar.install.summary.pack-download-failed": {
|
||||
"message": "Kunne ikke download pack"
|
||||
},
|
||||
"app.action-bar.install.summary.unexpected-error": {
|
||||
"message": "Noget gik galt"
|
||||
},
|
||||
"app.action-bar.install.unknown-instance": {
|
||||
"message": "Ukendt instance"
|
||||
},
|
||||
"app.action-bar.install.updating-shared-content": {
|
||||
"message": "Opdatere delt indhold"
|
||||
},
|
||||
"app.action-bar.make-primary-instance": {
|
||||
"message": "Gør til primære instance"
|
||||
},
|
||||
@@ -56,6 +122,15 @@
|
||||
"app.action-bar.view-logs": {
|
||||
"message": "Vis logs"
|
||||
},
|
||||
"app.ads-consent.accept": {
|
||||
"message": "Tillad alle"
|
||||
},
|
||||
"app.ads-consent.body": {
|
||||
"message": "Reklamer gør Modrinth muligt og finansiere skaber udbetalinger. Vores partnere må gemme og tilgå cookies i appen for at personliggøre reklamer og måle effektivitet."
|
||||
},
|
||||
"app.ads-consent.reject": {
|
||||
"message": "Afvis alle"
|
||||
},
|
||||
"app.appearance-settings.advanced-rendering.description": {
|
||||
"message": "Aktiverer avanceret gengivelse som sløringseffekter, der kan forårsage ydelsesproblemer uden hardwareaccelereret gengivelse."
|
||||
},
|
||||
@@ -98,9 +173,6 @@
|
||||
"app.appearance-settings.minimize-launcher.title": {
|
||||
"message": "Minimer launcher"
|
||||
},
|
||||
"app.appearance-settings.select-option": {
|
||||
"message": "Vælg en mulighed"
|
||||
},
|
||||
"app.appearance-settings.show-play-time.description": {
|
||||
"message": "Vis hvor meget tid du har brugt på at spille en instance."
|
||||
},
|
||||
@@ -146,9 +218,6 @@
|
||||
"app.browse.back-to-instance": {
|
||||
"message": "Tilbage til instance"
|
||||
},
|
||||
"app.browse.discover-content": {
|
||||
"message": "Opdag indhold"
|
||||
},
|
||||
"app.browse.discover-servers": {
|
||||
"message": "Opdag servere"
|
||||
},
|
||||
@@ -197,6 +266,9 @@
|
||||
"app.install.phase.preparing_java.downloading": {
|
||||
"message": "Downloader Java {version}"
|
||||
},
|
||||
"app.instance.admonitions.shared-instance.added-label": {
|
||||
"message": "Tilføjet"
|
||||
},
|
||||
"app.instance.confirm-delete.admonition-body": {
|
||||
"message": "Al' data for din instance vil blive permanent slettet, dette inkludere dine verdener, konfigurationer, og alt installeret indhold."
|
||||
},
|
||||
@@ -242,12 +314,51 @@
|
||||
"app.instance.mods.successfully-uploaded": {
|
||||
"message": "Uploadet"
|
||||
},
|
||||
"app.instance.share.locked.empty-description-suffix": {
|
||||
"message": "for at tilgå denne side."
|
||||
},
|
||||
"app.instance.share.locked.signed-out-heading": {
|
||||
"message": "Ikke logget ind"
|
||||
},
|
||||
"app.instance.share.locked.switch-account-button": {
|
||||
"message": "Skift konto"
|
||||
},
|
||||
"app.instance.share.locked.wrong-account-heading": {
|
||||
"message": "Forkert konto"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.effects-label": {
|
||||
"message": "Hvad vil der ske?"
|
||||
},
|
||||
"app.instance.share.remove-user-modal.user-avatar-alt": {
|
||||
"message": "{username}'s avatar"
|
||||
},
|
||||
"app.instance.share.sign-in.button": {
|
||||
"message": "Log ind"
|
||||
},
|
||||
"app.instance.shared-instance-already-installed.install-anyway": {
|
||||
"message": "Installer alligevel"
|
||||
},
|
||||
"app.instance.shared-instance-already-installed.instance": {
|
||||
"message": "Instans"
|
||||
},
|
||||
"app.instance.shared-instance-wrong-account.sign-in-as-label": {
|
||||
"message": "Log ind som"
|
||||
},
|
||||
"app.instance.worlds.add-server": {
|
||||
"message": "Tilføj server"
|
||||
},
|
||||
"app.instance.worlds.browse-servers": {
|
||||
"message": "Søg servere"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.delete-button": {
|
||||
"message": "Slet verden"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.title": {
|
||||
"message": "Slet verden"
|
||||
},
|
||||
"app.instance.worlds.delete-world-modal.warning-header": {
|
||||
"message": "Sletter {name}"
|
||||
},
|
||||
"app.instance.worlds.filter-modded": {
|
||||
"message": "Moddet"
|
||||
},
|
||||
@@ -266,18 +377,42 @@
|
||||
"app.instance.worlds.no-worlds-heading": {
|
||||
"message": "Ingen servere eller verdener tilføjet"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.remove-button": {
|
||||
"message": "Fjern server"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.title": {
|
||||
"message": "Fjern server"
|
||||
},
|
||||
"app.instance.worlds.remove-server-modal.warning-header": {
|
||||
"message": "Fjerner {name}"
|
||||
},
|
||||
"app.instance.worlds.search-worlds-placeholder": {
|
||||
"message": "Søg {count} verdener..."
|
||||
},
|
||||
"app.modal.install-to-play.additional-context": {
|
||||
"message": "Yderligere kontekst"
|
||||
},
|
||||
"app.modal.install-to-play.delete-instance": {
|
||||
"message": "Slet instans"
|
||||
},
|
||||
"app.modal.install-to-play.header": {
|
||||
"message": "Installer for at spille"
|
||||
},
|
||||
"app.modal.install-to-play.install-anyway": {
|
||||
"message": "Installer alligevel"
|
||||
},
|
||||
"app.modal.install-to-play.install-button": {
|
||||
"message": "Installer"
|
||||
},
|
||||
"app.modal.install-to-play.mod-count": {
|
||||
"message": "{count, plural, one {# mod} other {# mods}}"
|
||||
},
|
||||
"app.modal.install-to-play.report-image-too-large": {
|
||||
"message": "Filen overskrider 1MiB størrelse begrænsning"
|
||||
},
|
||||
"app.modal.install-to-play.report-reason.spam": {
|
||||
"message": "Spam"
|
||||
},
|
||||
"app.modal.install-to-play.shared-instance": {
|
||||
"message": "Delt instance"
|
||||
},
|
||||
@@ -287,15 +422,33 @@
|
||||
"app.modal.update-to-play.header": {
|
||||
"message": "Opdater for at spille"
|
||||
},
|
||||
"app.modal.update-to-play.update-required-description": {
|
||||
"message": "En opdatering er krævet for at spille {name}. Venligst opdater til den seneste version for at køre spillet."
|
||||
"app.modal.update-to-play.shared-instance-added-label": {
|
||||
"message": "Tilføjede"
|
||||
},
|
||||
"app.modal.update-to-play.shared-instance-removed-label": {
|
||||
"message": "Fjernede"
|
||||
},
|
||||
"app.modal.update-to-play.update-required": {
|
||||
"message": "Opdatering krævet"
|
||||
},
|
||||
"app.project.install-button.already-installed": {
|
||||
"message": "Dette projekt er allerede installeret"
|
||||
},
|
||||
"app.project.install-button.switch-version": {
|
||||
"message": "Skift version"
|
||||
},
|
||||
"app.project.install-context.back-to-browse": {
|
||||
"message": "Tilbage til udforsk"
|
||||
},
|
||||
"app.project.version.all-versions": {
|
||||
"message": "Alle versioner"
|
||||
},
|
||||
"app.project.version.download-in-browser": {
|
||||
"message": "Download i browser"
|
||||
},
|
||||
"app.project.version.installing": {
|
||||
"message": "Installere"
|
||||
},
|
||||
"app.project.versions.already-installed": {
|
||||
"message": "Allerede installeret"
|
||||
},
|
||||
@@ -356,6 +509,9 @@
|
||||
"app.skins.modal.add-title": {
|
||||
"message": "Tilføjer et skin"
|
||||
},
|
||||
"app.skins.modal.arm-style-section": {
|
||||
"message": "Arm stil"
|
||||
},
|
||||
"app.skins.modal.arm-style-slim": {
|
||||
"message": "Slank"
|
||||
},
|
||||
@@ -458,6 +614,12 @@
|
||||
"app.skins.title": {
|
||||
"message": "Skin vælger"
|
||||
},
|
||||
"app.skins.toggle-ears-features-off": {
|
||||
"message": "Slå fra"
|
||||
},
|
||||
"app.skins.toggle-ears-features-on": {
|
||||
"message": "Slå til"
|
||||
},
|
||||
"app.update-popup.body.download-complete": {
|
||||
"message": "Modrinth App v{version} er færdig med at download. Genindlæs for at opdatere nu, eller automatisk når du lukker Modrinth App."
|
||||
},
|
||||
@@ -506,6 +668,9 @@
|
||||
"app.world.world-item.players-online": {
|
||||
"message": "{count} online"
|
||||
},
|
||||
"content.shared-instance.delete-button": {
|
||||
"message": "Slet alligevel"
|
||||
},
|
||||
"friends.action.add-friend": {
|
||||
"message": "Tilføj en ven"
|
||||
},
|
||||
@@ -569,6 +734,27 @@
|
||||
"friends.sign-in-to-add-friends": {
|
||||
"message": "<link>Log ind på en Modrinth konto</link> for at tilføje venner og se hvad de spiller!"
|
||||
},
|
||||
"instance.action.create-shortcut": {
|
||||
"message": "Opret genvej"
|
||||
},
|
||||
"instance.action.export-modpack": {
|
||||
"message": "Eksporter modpack"
|
||||
},
|
||||
"instance.action.more-actions": {
|
||||
"message": "Flere handlinger"
|
||||
},
|
||||
"instance.action.open-folder": {
|
||||
"message": "Åbn mappe"
|
||||
},
|
||||
"instance.action.settings": {
|
||||
"message": "Instans indstillinger"
|
||||
},
|
||||
"instance.action.starting": {
|
||||
"message": "Starter..."
|
||||
},
|
||||
"instance.action.stopping": {
|
||||
"message": "Stopper..."
|
||||
},
|
||||
"instance.add-server.add-and-play": {
|
||||
"message": "Tilføj og spil"
|
||||
},
|
||||
@@ -611,6 +797,15 @@
|
||||
"instance.files.save-as": {
|
||||
"message": "Gem som..."
|
||||
},
|
||||
"instance.locked.delete-button": {
|
||||
"message": "Slet instans"
|
||||
},
|
||||
"instance.locked.play-tooltip": {
|
||||
"message": "Denne instans er blevet låst"
|
||||
},
|
||||
"instance.playtime.never-played": {
|
||||
"message": "Aldrig spillet"
|
||||
},
|
||||
"instance.server-modal.address": {
|
||||
"message": "Adresse"
|
||||
},
|
||||
@@ -782,6 +977,33 @@
|
||||
"instance.settings.tabs.window.width.enter": {
|
||||
"message": "Indtast bredde..."
|
||||
},
|
||||
"instance.shared-instance.error.title": {
|
||||
"message": "Noget gik galt"
|
||||
},
|
||||
"instance.shared-instance.publish-review.added-label": {
|
||||
"message": "Tilføjede"
|
||||
},
|
||||
"instance.shared-instance.publish-review.admonition-header": {
|
||||
"message": "Skub opdatering til spillere"
|
||||
},
|
||||
"instance.shared-instance.publish-review.config-title-v2": {
|
||||
"message": "Vælg konfigurations filer"
|
||||
},
|
||||
"instance.shared-instance.publish-review.header": {
|
||||
"message": "Gennemså ændringer"
|
||||
},
|
||||
"instance.shared-instance.publish-review.publish-button": {
|
||||
"message": "Skub opdatering"
|
||||
},
|
||||
"instance.shared-instance.publish-review.removed-label": {
|
||||
"message": "Fjernede"
|
||||
},
|
||||
"instance.shared-instance.unavailable.locked-title": {
|
||||
"message": "Instans låst"
|
||||
},
|
||||
"instance.shared-instance.unavailable.title": {
|
||||
"message": "Delt instans er ikke længere tilgængelig"
|
||||
},
|
||||
"instance.worlds.a_minecraft_server": {
|
||||
"message": "En Minecraft server"
|
||||
},
|
||||
@@ -791,6 +1013,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Kopier adresse"
|
||||
},
|
||||
"instance.worlds.create_shortcut": {
|
||||
"message": "Opret genvej"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Vis ikke på forsiden"
|
||||
},
|
||||
@@ -842,6 +1067,54 @@
|
||||
"minecraft-account.sign-in": {
|
||||
"message": "Log ind på Minecraft"
|
||||
},
|
||||
"minecraft-required.description-header": {
|
||||
"message": "Log ind med en Microsoft konto"
|
||||
},
|
||||
"minecraft-required.dont-have-account": {
|
||||
"message": "Har du ikke en konto?"
|
||||
},
|
||||
"minecraft-required.get-minecraft": {
|
||||
"message": "Få Minecraft"
|
||||
},
|
||||
"minecraft-required.get-support": {
|
||||
"message": "Få hjælp"
|
||||
},
|
||||
"minecraft-required.header": {
|
||||
"message": "Minecraft påkrævet"
|
||||
},
|
||||
"minecraft-required.sign-in": {
|
||||
"message": "Log ind på Microsoft"
|
||||
},
|
||||
"modal.modrinth-account-required.cancel-button": {
|
||||
"message": "Annuller"
|
||||
},
|
||||
"modal.modrinth-account-required.continue-in-browser-heading": {
|
||||
"message": "Fortsæt i din browser"
|
||||
},
|
||||
"modal.modrinth-account-required.create-account-button": {
|
||||
"message": "Opret en konto"
|
||||
},
|
||||
"modal.modrinth-account-required.header": {
|
||||
"message": "Konto påkrævet"
|
||||
},
|
||||
"modal.modrinth-account-required.open-browser-again-button": {
|
||||
"message": "Åbn browser igen"
|
||||
},
|
||||
"modal.modrinth-account-required.sign-in-button": {
|
||||
"message": "Log ind på Modrinth"
|
||||
},
|
||||
"modal.modrinth-account-required.sign-in-heading": {
|
||||
"message": "Log ind med en Modrinth konto"
|
||||
},
|
||||
"modal.modrinth-account-required.signing-in-header": {
|
||||
"message": "Logger ind"
|
||||
},
|
||||
"modal.modrinth-account-required.support-prompt": {
|
||||
"message": "Har du problemer med at logge ind? <support>Få hjælp</support>"
|
||||
},
|
||||
"modal.modrinth-account-required.waiting-for-browser": {
|
||||
"message": "Venter på browser bekræftigelse..."
|
||||
},
|
||||
"search.filter.locked.instance": {
|
||||
"message": "Givet af instanceen"
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user