Compare commits
228 Commits
fetch/incl
...
cal/medal-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42a2451990 | ||
|
|
54958491b3 | ||
|
|
b497c944a0 | ||
|
|
e89ebf6915 | ||
|
|
5b46f521b7 | ||
|
|
e010e617c1 | ||
|
|
0b3cbbd37a | ||
|
|
ea1789ebf6 | ||
|
|
6401a8937b | ||
|
|
2da2b4aec7 | ||
|
|
d1a478dbf2 | ||
|
|
99493b9917 | ||
|
|
72a52eb7b1 | ||
|
|
b33e12c71d | ||
|
|
82d86839c7 | ||
|
|
3a20e15340 | ||
|
|
1c89b84314 | ||
|
|
6387fb21c6 | ||
|
|
c7d0839bfb | ||
|
|
175b90be5a | ||
|
|
13103b4950 | ||
|
|
8804478221 | ||
|
|
b8982a6d17 | ||
|
|
ff88724d01 | ||
|
|
7dffb352d5 | ||
|
|
1df6e29aa1 | ||
|
|
5deb4179ad | ||
|
|
358cf31c87 | ||
|
|
6db1d66591 | ||
|
|
8052fda840 | ||
|
|
15892a88d3 | ||
|
|
32793c50e1 | ||
|
|
0e0ca1971a | ||
|
|
bb9af18eed | ||
|
|
d4516d3527 | ||
|
|
87de47fe5e | ||
|
|
7d76fe1b6a | ||
|
|
ae25a15abd | ||
|
|
0f755b94ce | ||
|
|
bcf46d440b | ||
|
|
526561f2de | ||
|
|
a8caa1afc3 | ||
|
|
98e9a8473d | ||
|
|
936395484e | ||
|
|
0c3e23db96 | ||
|
|
013ba4d86d | ||
|
|
93813c448c | ||
|
|
c20b869e62 | ||
|
|
56c556821b | ||
|
|
44267619b6 | ||
|
|
90043fe84d | ||
|
|
a6a98ff63e | ||
|
|
911652133b | ||
|
|
cee1b5f522 | ||
|
|
62f5a23fcb | ||
|
|
eb595cdc3e | ||
|
|
572cd065ed | ||
|
|
76dc8a0897 | ||
|
|
4723de6269 | ||
|
|
e15fa35bad | ||
|
|
2cc6bc8ce4 | ||
|
|
5d19d31b2c | ||
|
|
c1b95ede07 | ||
|
|
058185c7fd | ||
|
|
6fb125cf0f | ||
|
|
a945e9b005 | ||
|
|
b943638afb | ||
|
|
207dc0e2bb | ||
|
|
359fbd4738 | ||
|
|
f7700acce4 | ||
|
|
87a3e2d022 | ||
|
|
5d17663040 | ||
|
|
cff3c72f94 | ||
|
|
fadf475f06 | ||
|
|
7228499737 | ||
|
|
bca467a634 | ||
|
|
cb72d2ac80 | ||
|
|
3c79607d1f | ||
|
|
36ad1f16e4 | ||
|
|
5d4f334505 | ||
|
|
1fdb5ba748 | ||
|
|
26df6f51ef | ||
|
|
6caf794ae1 | ||
|
|
2692953e31 | ||
|
|
242fd713ab | ||
|
|
7a12c4d5e2 | ||
|
|
f256ef43c0 | ||
|
|
e0cde2d6ff | ||
|
|
e4e77dc0d2 | ||
|
|
8ba6467f21 | ||
|
|
088cb54317 | ||
|
|
c47bcf665d | ||
|
|
bc90c27e27 | ||
|
|
c1be57773a | ||
|
|
315c68912c | ||
|
|
559d203996 | ||
|
|
54522518c3 | ||
|
|
bacb1561d5 | ||
|
|
b8521f926f | ||
|
|
b29672f4b4 | ||
|
|
a32fe6a41f | ||
|
|
0e35135093 | ||
|
|
31ecace083 | ||
|
|
e5b134f8f4 | ||
|
|
139a4863d1 | ||
|
|
8faea1663a | ||
|
|
ece8a07486 | ||
|
|
0030f35d0c | ||
|
|
1e24225350 | ||
|
|
e84a178586 | ||
|
|
0a83ed965e | ||
|
|
30035a9a1c | ||
|
|
512d456c66 | ||
|
|
bff26af465 | ||
|
|
f4d0f14cb6 | ||
|
|
55916b6bda | ||
|
|
a38e1dee1f | ||
|
|
ef76a81cd5 | ||
|
|
9dc5644264 | ||
|
|
8e35cf6957 | ||
|
|
ae1c3d6531 | ||
|
|
4964c8d373 | ||
|
|
497b2e977e | ||
|
|
f95d0d78f2 | ||
|
|
94a7d13af8 | ||
|
|
3a10e63756 | ||
|
|
238138d56e | ||
|
|
1846c59733 | ||
|
|
f1207f0a3a | ||
|
|
26e964174d | ||
|
|
897418ead3 | ||
|
|
eef09e1ffe | ||
|
|
fdb2b1195e | ||
|
|
4b3e036e2a | ||
|
|
3233e7fc54 | ||
|
|
dd98a1316a | ||
|
|
e5030a8fbe | ||
|
|
f549560e47 | ||
|
|
33d26238ce | ||
|
|
bcec478a64 | ||
|
|
8971d39683 | ||
|
|
1c1631f131 | ||
|
|
14b1ff79e0 | ||
|
|
479aaf503b | ||
|
|
240cccf8a1 | ||
|
|
2599dc2672 | ||
|
|
e2668f20b7 | ||
|
|
cf767c7ef2 | ||
|
|
14a7787e3d | ||
|
|
db963eb5de | ||
|
|
a1812cd954 | ||
|
|
5ed9d1749a | ||
|
|
17ca209862 | ||
|
|
03192c1dfd | ||
|
|
6f03fae233 | ||
|
|
22fc0c994d | ||
|
|
a1ccbc5757 | ||
|
|
053cf10198 | ||
|
|
257efd8ad7 | ||
|
|
b75cfc063b | ||
|
|
2d8420131d | ||
|
|
c793b68aed | ||
|
|
47af459f24 | ||
|
|
f10e0f2bf1 | ||
|
|
569d60cb57 | ||
|
|
74d36a6a2d | ||
|
|
ced073d26c | ||
|
|
cc34e69524 | ||
|
|
d4864deac5 | ||
|
|
125207880d | ||
|
|
a8f17f40f5 | ||
|
|
dbde3c4669 | ||
|
|
ba4fecb0cb | ||
|
|
ef04dcc37b | ||
|
|
65126b3a23 | ||
|
|
58495e6276 | ||
|
|
706976439d | ||
|
|
0a9ffd3dc8 | ||
|
|
fb30c0ba2b | ||
|
|
97e4d8e132 | ||
|
|
5bdff3929b | ||
|
|
a08562bfe2 | ||
|
|
2b4319ea55 | ||
|
|
c32405720d | ||
|
|
b9ba3cd3e8 | ||
|
|
31381c860b | ||
|
|
e410a07cac | ||
|
|
9f93cd8705 | ||
|
|
dd391be095 | ||
|
|
f84f8c1c2b | ||
|
|
301967d204 | ||
|
|
c9b98a6154 | ||
|
|
ab8e474339 | ||
|
|
8a26011e76 | ||
|
|
d4de1dc9a1 | ||
|
|
4e3bd4e282 | ||
|
|
d24528f6a6 | ||
|
|
1b1d41605b | ||
|
|
6955731def | ||
|
|
4386891716 | ||
|
|
6741aba880 | ||
|
|
ee8ee7af82 | ||
|
|
a2e323c9ee | ||
|
|
f8fb23e05f | ||
|
|
a3839461cf | ||
|
|
858c7e393f | ||
|
|
0278241006 | ||
|
|
3afb682fc6 | ||
|
|
06f1df1995 | ||
|
|
3489771d2e | ||
|
|
448ae5a2b7 | ||
|
|
72340790e5 | ||
|
|
c9423fe478 | ||
|
|
02a850ae63 | ||
|
|
ede6d0c3cc | ||
|
|
7685989a8c | ||
|
|
4e8ebb5e5c | ||
|
|
3f77ab19ed | ||
|
|
d3d0c8c523 | ||
|
|
4e093131f3 | ||
|
|
6ca8a4e5fd | ||
|
|
63b15ded60 | ||
|
|
85e65aeffe | ||
|
|
ad44398492 | ||
|
|
a4ba41bf15 | ||
|
|
4441be5380 | ||
|
|
c0accb42fa | ||
|
|
7223c2b197 |
@@ -1,6 +1,9 @@
|
||||
# Windows has stack overflows when calling from Tauri, so we increase compiler size
|
||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||
[target.'cfg(windows)']
|
||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
||||
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld"
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
|
||||
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@@ -0,0 +1 @@
|
||||
.gitignore
|
||||
@@ -14,5 +14,5 @@ max_line_length = 100
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
||||
[*.{rs,java,kts}]
|
||||
indent_size = 4
|
||||
|
||||
34
.gitattributes
vendored
34
.gitattributes
vendored
@@ -1 +1,35 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
# SQLx calculates a checksum of migration scripts at build time to compare
|
||||
# it with the checksum of the applied migration for the same version at
|
||||
# runtime, to know if the migration script has been changed, and thus the
|
||||
# DB schema went out of sync with the code.
|
||||
#
|
||||
# However, such checksum treats the script as a raw byte stream, taking
|
||||
# into account inconsequential differences like different line endings
|
||||
# in different OSes. When combined with Git's EOL conversion and mixed
|
||||
# native and cross-compilation scenarios, this leads to existing
|
||||
# migrations that didn't change having potentially different checksums
|
||||
# according to the environment they were built in, which can break the
|
||||
# migration system when deploying the Modrinth App, rendering it
|
||||
# unusable.
|
||||
#
|
||||
# The gitattribute above ensures that all text files are checked out
|
||||
# with LF line endings, but widely deployed app versions were built
|
||||
# without this attribute set, which left such line endings variable to
|
||||
# the platform. Thus, there is no perfect solution to this problem:
|
||||
# forcing CRLF here would break Linux and macOS users, forcing LF
|
||||
# breaks Windows users, and leaving it unspecified may still lead to
|
||||
# line ending differences when cross-compiling from Linux to Windows
|
||||
# or vice versa, or having Git configured with different line
|
||||
# conversion settings. Moreover, there is no `eol=native` attribute,
|
||||
# and using CI-only scripts to convert line endings would make the
|
||||
# builds differ between CI and most local environments. So, let's pick
|
||||
# the least bad option: let Git handle line endings using its
|
||||
# configuration by leaving it unspecified, which works fine as long as
|
||||
# people don't mess with Git's line ending settings, which is the vast
|
||||
# majority of cases.
|
||||
/packages/app-lib/migrations/20240711194701_init.sql !eol
|
||||
/packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol
|
||||
/packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol
|
||||
/packages/app-lib/migrations/20241222013857_feature-flags.sql !eol
|
||||
|
||||
13
.github/workflows/daedalus-docker.yml
vendored
13
.github/workflows/daedalus-docker.yml
vendored
@@ -22,23 +22,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Fetch docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/modrinth/daedalus
|
||||
- name: Login to GitHub Images
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./apps/daedalus_client/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=ghcr.io/modrinth/daedalus:main
|
||||
cache-to: type=inline
|
||||
|
||||
18
.github/workflows/labrinth-docker.yml
vendored
18
.github/workflows/labrinth-docker.yml
vendored
@@ -18,30 +18,28 @@ on:
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/labrinth
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Fetch docker metadata
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/modrinth/labrinth
|
||||
- name: Login to GitHub Images
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./apps/labrinth/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=ghcr.io/modrinth/labrinth:main
|
||||
cache-to: type=inline
|
||||
|
||||
152
.github/workflows/theseus-build.yml
vendored
Normal file
152
.github/workflows/theseus-build.yml
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
name: Modrinth App build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- .github/workflows/theseus-build.yml
|
||||
- 'apps/app/**'
|
||||
- 'apps/app-frontend/**'
|
||||
- 'packages/app-lib/**'
|
||||
- 'packages/app-macros/**'
|
||||
- 'packages/assets/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/utils/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign-windows-binaries:
|
||||
description: Sign Windows binaries
|
||||
type: boolean
|
||||
default: true
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
include:
|
||||
- platform: macos-latest
|
||||
artifact-target-name: universal-apple-darwin
|
||||
- platform: windows-latest
|
||||
artifact-target-name: x86_64-pc-windows-msvc
|
||||
- platform: ubuntu-22.04
|
||||
artifact-target-name: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🧰 Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ''
|
||||
target: ${{ startsWith(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: 🧰 Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🧰 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: 🧰 Install Linux build dependencies
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: 🧰 Setup Dasel
|
||||
uses: jaxxstorm/action-install-gh-release@v2.1.0
|
||||
with:
|
||||
repo: TomWright/dasel
|
||||
tag: v2.8.1
|
||||
extension-matching: disable
|
||||
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
chmod: 0755
|
||||
|
||||
- name: ⚙️ Set application version and environment
|
||||
shell: bash
|
||||
run: |
|
||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||
echo "Setting application version to $APP_VERSION"
|
||||
dasel put -f apps/app/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
||||
|
||||
cp packages/app-lib/.env.prod packages/app-lib/.env
|
||||
|
||||
- name: 💨 Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@v1.8
|
||||
|
||||
- name: 🧰 Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ✍️ Set up Windows code signing
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then
|
||||
choco install jsign --ignore-dependencies # GitHub runners come with a global Java installation already
|
||||
else
|
||||
dasel delete -f apps/app/tauri-release.conf.json 'bundle.windows.signCommand'
|
||||
fi
|
||||
|
||||
- name: 🔨 Build macOS app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
env:
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Linux app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Windows app
|
||||
run: |
|
||||
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
|
||||
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
|
||||
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
DIGICERT_ONE_SIGNER_API_KEY: ${{ secrets.DIGICERT_ONE_SIGNER_API_KEY }}
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: 📤 Upload app bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: App bundle (${{ matrix.artifact-target-name }})
|
||||
path: |
|
||||
target/release/bundle/appimage/Modrinth App_*.AppImage*
|
||||
target/release/bundle/deb/Modrinth App_*.deb*
|
||||
target/release/bundle/rpm/Modrinth App-*.rpm*
|
||||
target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz*
|
||||
target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg*
|
||||
target/release/bundle/nsis/Modrinth App_*-setup.exe*
|
||||
target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip*
|
||||
245
.github/workflows/theseus-release.yml
vendored
245
.github/workflows/theseus-release.yml
vendored
@@ -1,157 +1,118 @@
|
||||
name: 'Modrinth App build'
|
||||
name: Modrinth App release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- .github/workflows/theseus-release.yml
|
||||
- 'apps/app/**'
|
||||
- 'apps/app-frontend/**'
|
||||
- 'apps/labrinth/src/common/**'
|
||||
- 'apps/labrinth/Cargo.toml'
|
||||
- 'packages/app-lib/**'
|
||||
- 'packages/app-macros/**'
|
||||
- 'packages/assets/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/utils/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version-tag:
|
||||
description: Version tag to release to the wide public
|
||||
type: string
|
||||
required: true
|
||||
release-notes:
|
||||
description: Release notes to include in the Tauri version manifest
|
||||
default: A new release of the Modrinth App is available!
|
||||
type: string
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
release:
|
||||
name: Release Modrinth App
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
|
||||
WINDOWS_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-pc-windows-msvc)
|
||||
MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME: App bundle (universal-apple-darwin)
|
||||
LAUNCHER_FILES_BUCKET_BASE_URL: https://launcher-files.modrinth.com
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Rust setup (mac)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: 📥 Download Modrinth App artifacts
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||
workflow: theseus-build.yml
|
||||
workflow_conclusion: success
|
||||
event: push
|
||||
branch: ${{ inputs.version-tag }}
|
||||
use_unzip: true
|
||||
|
||||
- name: Rust setup
|
||||
if: "!startsWith(matrix.platform, 'macos')"
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Setup rust cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
target/**
|
||||
!target/*/release/bundle/*/*.dmg
|
||||
!target/*/release/bundle/*/*.app.tar.gz
|
||||
!target/*/release/bundle/*/*.app.tar.gz.sig
|
||||
!target/release/bundle/*/*.dmg
|
||||
!target/release/bundle/*/*.app.tar.gz
|
||||
!target/release/bundle/*/*.app.tar.gz.sig
|
||||
|
||||
!target/release/bundle/appimage/*.AppImage
|
||||
!target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
!target/release/bundle/appimage/*.AppImage.tar.gz.sig
|
||||
!target/release/bundle/deb/*.deb
|
||||
!target/release/bundle/rpm/*.rpm
|
||||
|
||||
!target/release/bundle/msi/*.msi
|
||||
!target/release/bundle/msi/*.msi.zip
|
||||
!target/release/bundle/msi/*.msi.zip.sig
|
||||
|
||||
!target/release/bundle/nsis/*.exe
|
||||
!target/release/bundle/nsis/*.nsis.zip
|
||||
!target/release/bundle/nsis/*.nsis.zip.sig
|
||||
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-rust-target-
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: build app (macos)
|
||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config "tauri-release.conf.json"
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
- name: 🛠️ Generate version manifest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VERSION_TAG: ${{ inputs.version-tag }}
|
||||
RELEASE_NOTES: ${{ inputs.release-notes }}
|
||||
run: |
|
||||
# Reference: https://tauri.app/plugin/updater/#server-support
|
||||
jq -nc \
|
||||
--arg versionTag "${VERSION_TAG#v}" \
|
||||
--arg releaseNotes "$RELEASE_NOTES" \
|
||||
--rawfile macOsAarch64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
|
||||
--rawfile macOsX64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
|
||||
--rawfile linuxX64UpdateArtifactSignature "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION_TAG#v}_amd64.AppImage.tar.gz.sig" \
|
||||
--rawfile windowsX64UpdateArtifactSignature "${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/nsis/Modrinth App_${VERSION_TAG#v}_x64-setup.nsis.zip.sig" \
|
||||
'{
|
||||
"version": $versionTag,
|
||||
"notes": $releaseNotes,
|
||||
"pub_date": now | todateiso8601,
|
||||
"platforms": {
|
||||
"darwin-aarch64": {
|
||||
"signature": $macOsAarch64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App.app.tar.gz")",
|
||||
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App_" + $versionTag + "_universal.dmg")"]
|
||||
},
|
||||
"darwin-x86_64": {
|
||||
"signature": $macOsX64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App.app.tar.gz")",
|
||||
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App_" + $versionTag + "_universal.dmg")"]
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"signature": $linuxX64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.AppImage.tar.gz")",
|
||||
"install_urls": [
|
||||
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.deb")",
|
||||
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.AppImage")",
|
||||
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App-" + $versionTag + "-1.x86_64.rpm")"
|
||||
]
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": $windowsX64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/windows/\("Modrinth App_" + $versionTag + "_x64-setup.nsis.zip")",
|
||||
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/windows/\("Modrinth App_" + $versionTag + "_x64-setup.exe")"]
|
||||
}
|
||||
}
|
||||
}' > updates.json
|
||||
|
||||
- name: build app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
|
||||
id: build_os
|
||||
if: "!startsWith(matrix.platform, 'macos')"
|
||||
echo "Generated manifest for version ${VERSION_TAG}:"
|
||||
cat updates.json
|
||||
|
||||
- name: 📤 Upload release artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VERSION_TAG: ${{ inputs.version-tag }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.LAUNCHER_FILES_BUCKET_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LAUNCHER_FILES_BUCKET_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ secrets.LAUNCHER_FILES_BUCKET_NAME }}
|
||||
AWS_REGION: ${{ secrets.LAUNCHER_FILES_BUCKET_REGION }}
|
||||
AWS_ENDPOINT_URL: ${{ secrets.LAUNCHER_FILES_BUCKET_ENDPOINT_URL }}
|
||||
AWS_PAGER: ''
|
||||
# Work around incompatible checksum behavior with some S3-like object storage providers,
|
||||
# such as Cloudflare R2. See:
|
||||
# - https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
||||
# - https://developers.cloudflare.com/r2/examples/aws/aws-sdk-java/
|
||||
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
|
||||
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
|
||||
run: |
|
||||
for macosBundleType in 'macos' 'dmg'; do
|
||||
aws s3 cp --recursive \
|
||||
"${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/${macosBundleType}" \
|
||||
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/macos"
|
||||
done
|
||||
|
||||
- name: upload ${{ matrix.platform }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.platform }}
|
||||
path: |
|
||||
target/*/release/bundle/*/*.dmg
|
||||
target/*/release/bundle/*/*.app.tar.gz
|
||||
target/*/release/bundle/*/*.app.tar.gz.sig
|
||||
target/release/bundle/*/*.dmg
|
||||
target/release/bundle/*/*.app.tar.gz
|
||||
target/release/bundle/*/*.app.tar.gz.sig
|
||||
for linuxBundleType in 'appimage' 'deb' 'rpm'; do
|
||||
aws s3 cp --recursive \
|
||||
"${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/${linuxBundleType}" \
|
||||
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/linux"
|
||||
done
|
||||
|
||||
target/release/bundle/*/*.AppImage
|
||||
target/release/bundle/*/*.AppImage.tar.gz
|
||||
target/release/bundle/*/*.AppImage.tar.gz.sig
|
||||
target/release/bundle/*/*.deb
|
||||
target/release/bundle/*/*.rpm
|
||||
for windowsBundleType in 'nsis'; do
|
||||
aws s3 cp --recursive \
|
||||
"${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/${windowsBundleType}" \
|
||||
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/windows"
|
||||
done
|
||||
|
||||
target/release/bundle/msi/*.msi
|
||||
target/release/bundle/msi/*.msi.zip
|
||||
target/release/bundle/msi/*.msi.zip.sig
|
||||
|
||||
target/release/bundle/nsis/*.exe
|
||||
target/release/bundle/nsis/*.nsis.zip
|
||||
target/release/bundle/nsis/*.nsis.zip.sig
|
||||
aws s3 cp updates.json "s3://${AWS_BUCKET}"
|
||||
|
||||
105
.github/workflows/turbo-ci.yml
vendored
105
.github/workflows/turbo-ci.yml
vendored
@@ -2,7 +2,7 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
@@ -10,71 +10,78 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build, Test, and Lint
|
||||
name: Lint and Test
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
# Ensure pnpm output is colored in GitHub Actions logs
|
||||
FORCE_COLOR: 3
|
||||
# Make cargo nextest successfully ignore projects without tests
|
||||
NEXTEST_NO_TESTS: pass
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install build dependencies
|
||||
- name: 🧰 Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Setup Node.JS environment
|
||||
- name: 🧰 Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🧰 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
- name: 🧰 Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
rustflags: ''
|
||||
components: clippy, rustfmt
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
- name: 🧰 Setup nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||
# back to a cached cargo install
|
||||
- name: 🧰 Setup cargo-sqlx
|
||||
uses: taiki-e/cache-cargo-install-action@v2
|
||||
with:
|
||||
tool: sqlx-cli
|
||||
locked: false
|
||||
no-default-features: true
|
||||
features: rustls,postgres
|
||||
|
||||
- name: 💨 Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@v1.8
|
||||
|
||||
- name: 🧰 Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
- name: ⚙️ Start services
|
||||
run: docker compose up --wait
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
- name: ⚙️ Setup Labrinth environment and database
|
||||
working-directory: apps/labrinth
|
||||
run: |
|
||||
cp .env.local .env
|
||||
sqlx database setup
|
||||
|
||||
- name: Start docker compose
|
||||
run: docker compose up -d
|
||||
- name: ⚙️ Set app environment
|
||||
working-directory: packages/app-lib
|
||||
run: cp .env.staging .env
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
||||
- name: 🔍 Lint and test
|
||||
run: pnpm run ci
|
||||
|
||||
- name: 🔍 Verify intl:extract has been run
|
||||
run: |
|
||||
pnpm intl:extract
|
||||
git diff --exit-code --color */*/src/locales/en-US/index.json
|
||||
|
||||
2
.idea/code.iml
generated
2
.idea/code.iml
generated
@@ -17,4 +17,4 @@
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
2
.idea/modules.xml
generated
2
.idea/modules.xml
generated
@@ -5,4 +5,4 @@
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CommitMessageInspectionProfile">
|
||||
<profile version="1.0">
|
||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
|
||||
1009
Cargo.lock
generated
1009
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
127
Cargo.toml
127
Cargo.toml
@@ -10,6 +10,9 @@ members = [
|
||||
"packages/daedalus",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
actix-cors = "0.7.1"
|
||||
actix-files = "0.6.6"
|
||||
@@ -21,7 +24,8 @@ actix-web-prom = "0.10.0"
|
||||
actix-ws = "0.3.0"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async-compression = { version = "0.4.23", default-features = false }
|
||||
async_zip = "0.0.17"
|
||||
async-compression = { version = "0.4.25", default-features = false }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
@@ -31,18 +35,20 @@ async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
||||
"futures-03-sink",
|
||||
] }
|
||||
async-walkdir = "2.1.0"
|
||||
async_zip = "0.0.17"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.0"
|
||||
bitflags = "2.9.1"
|
||||
bytemuck = "1.23.0"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.41"
|
||||
clap = "4.5.38"
|
||||
clickhouse = "0.13.2"
|
||||
clap = "4.5.40"
|
||||
clickhouse = "0.13.3"
|
||||
color-thief = "0.2.2"
|
||||
console-subscriber = "0.4.1"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
dashmap = "6.1.0"
|
||||
data-url = "0.3.1"
|
||||
deadpool-redis = "0.21.1"
|
||||
dirs = "6.0.0"
|
||||
discord-rich-presence = "0.2.5"
|
||||
@@ -50,16 +56,25 @@ dotenv-build = "0.1.1"
|
||||
dotenvy = "0.15.7"
|
||||
dunce = "1.0.5"
|
||||
either = "1.15.0"
|
||||
encoding_rs = "0.8.35"
|
||||
enumset = "1.1.6"
|
||||
flate2 = "1.1.1"
|
||||
flate2 = "1.1.2"
|
||||
fs4 = { version = "0.13.1", default-features = false }
|
||||
futures = { version = "0.3.31", default-features = false }
|
||||
futures-util = "0.3.31"
|
||||
hashlink = "0.10.0"
|
||||
heck = "0.5.0"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25.2"
|
||||
hmac = "0.12.1"
|
||||
hyper-tls = "0.6.0"
|
||||
hyper-util = "0.1.11"
|
||||
hyper = "1.6.0"
|
||||
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||
"http1",
|
||||
"native-tokio",
|
||||
"ring",
|
||||
"tls12",
|
||||
] }
|
||||
hyper-util = "0.1.14"
|
||||
iana-time-zone = "0.1.63"
|
||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||
indexmap = "2.9.0"
|
||||
@@ -67,7 +82,7 @@ indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
jemalloc_pprof = "0.7.0"
|
||||
json-patch = { version = "4.0.0", default-features = false }
|
||||
lettre = { version = "0.11.16", default-features = false, features = [
|
||||
lettre = { version = "0.11.17", default-features = false, features = [
|
||||
"builder",
|
||||
"hostname",
|
||||
"pool",
|
||||
@@ -84,26 +99,29 @@ notify = { version = "8.0.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
||||
p256 = "0.13.2"
|
||||
paste = "1.0.15"
|
||||
phf = { version = "0.12.1", features = ["macros"] }
|
||||
png = "0.17.16"
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.37.5"
|
||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
redis = "0.31.0"
|
||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
reqwest = { version = "0.12.20", default-features = false }
|
||||
rgb = "0.8.50"
|
||||
rust_decimal = { version = "1.37.2", features = [
|
||||
"serde-with-float",
|
||||
"serde-with-str",
|
||||
] }
|
||||
rust_iso3166 = "0.1.14"
|
||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||
"fail-on-err",
|
||||
"tags",
|
||||
"tokio-rustls-tls",
|
||||
] }
|
||||
rust_decimal = { version = "1.37.1", features = [
|
||||
"serde-with-float",
|
||||
"serde-with-str",
|
||||
] }
|
||||
rust_iso3166 = "0.1.14"
|
||||
rusty-money = "0.4.1"
|
||||
sentry = { version = "0.38.1", default-features = false, features = [
|
||||
sentry = { version = "0.41.0", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
@@ -111,39 +129,40 @@ sentry = { version = "0.38.1", default-features = false, features = [
|
||||
"reqwest",
|
||||
"rustls",
|
||||
] }
|
||||
sentry-actix = "0.38.1"
|
||||
sentry-actix = "0.41.0"
|
||||
serde = "1.0.219"
|
||||
serde-xml-rs = "0.8.0" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
serde_bytes = "0.11.17"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_with = "3.12.0"
|
||||
serde_with = "3.13.0"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
spdx = "0.10.8"
|
||||
sqlx = { version = "0.8.5", default-features = false }
|
||||
sysinfo = { version = "0.35.1", default-features = false }
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
sysinfo = { version = "0.35.2", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tauri = "2.5.1"
|
||||
tauri-build = "2.2.0"
|
||||
tauri-plugin-deep-link = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.1"
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-single-instance = "2.2.3"
|
||||
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
||||
tauri = "2.6.1"
|
||||
tauri-build = "2.3.0"
|
||||
tauri-plugin-deep-link = "2.4.0"
|
||||
tauri-plugin-dialog = "2.3.0"
|
||||
tauri-plugin-http = "2.5.0"
|
||||
tauri-plugin-opener = "2.4.0"
|
||||
tauri-plugin-os = "2.3.0"
|
||||
tauri-plugin-single-instance = "2.3.0"
|
||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"zip",
|
||||
] }
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
tauri-plugin-window-state = "2.3.0"
|
||||
tempfile = "3.20.0"
|
||||
theseus = { path = "packages/app-lib" }
|
||||
thiserror = "2.0.12"
|
||||
tikv-jemalloc-ctl = "0.6.0"
|
||||
tikv-jemallocator = "0.6.0"
|
||||
tokio = "1.45.0"
|
||||
tokio = "1.45.1"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-util = "0.7.15"
|
||||
totp-rs = "5.7.0"
|
||||
@@ -153,14 +172,14 @@ tracing-error = "0.2.1"
|
||||
tracing-subscriber = "0.3.19"
|
||||
url = "2.5.4"
|
||||
urlencoding = "2.1.3"
|
||||
uuid = "1.16.0"
|
||||
uuid = "1.17.0"
|
||||
validator = "0.20.0"
|
||||
webp = { version = "0.3.0", default-features = false }
|
||||
whoami = "1.6.0"
|
||||
winreg = "0.55.0"
|
||||
woothee = "0.13.0"
|
||||
yaserde = "0.12.0"
|
||||
zip = { version = "3.0.0", default-features = false, features = [
|
||||
zip = { version = "4.2.0", default-features = false, features = [
|
||||
"bzip2",
|
||||
"deflate",
|
||||
"deflate64",
|
||||
@@ -168,8 +187,46 @@ zip = { version = "3.0.0", default-features = false, features = [
|
||||
] }
|
||||
zxcvbn = "3.1.0"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
bool_to_int_with_if = "warn"
|
||||
borrow_as_ptr = "warn"
|
||||
cfg_not_test = "warn"
|
||||
clear_with_drain = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
collection_is_never_read = "warn"
|
||||
dbg_macro = "warn"
|
||||
default_trait_access = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
filter_map_next = "warn"
|
||||
flat_map_option = "warn"
|
||||
format_push_string = "warn"
|
||||
get_unwrap = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
manual_assert = "warn"
|
||||
manual_instant_elapsed = "warn"
|
||||
manual_is_variant_and = "warn"
|
||||
manual_let_else = "warn"
|
||||
map_unwrap_or = "warn"
|
||||
match_bool = "warn"
|
||||
needless_collect = "warn"
|
||||
negative_feature_names = "warn"
|
||||
non_std_lazy_statics = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
read_zero_byte_vec = "warn"
|
||||
redundant_clone = "warn"
|
||||
redundant_feature_names = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
todo = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
|
||||
[workspace.lints.rust]
|
||||
# Turn warnings into errors by default
|
||||
warnings = "deny"
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "21db186" }
|
||||
|
||||
# Optimize for speed and reduce size on release builds
|
||||
[profile.release]
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
**/dist
|
||||
*.gltf
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.9.5",
|
||||
"version": "1.0.0-local",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -9,26 +9,31 @@
|
||||
"tsc:check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"test": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
"ofetch": "^1.3.4",
|
||||
"pinia": "^2.1.7",
|
||||
"posthog-js": "^1.158.2",
|
||||
"three": "^0.172.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-multiselect": "3.0.0",
|
||||
@@ -39,11 +44,12 @@
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@nuxt/eslint-config": "^0.5.6",
|
||||
"@taijased/vue-render-tracker": "^1.0.7",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"eslint-plugin-turbo": "^2.1.1",
|
||||
"eslint-plugin-turbo": "^2.5.4",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.74.1",
|
||||
@@ -51,8 +57,7 @@
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.6",
|
||||
"vue-tsc": "^2.1.6",
|
||||
"@taijased/vue-render-tracker": "^1.0.7"
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
"web-types": "../../web-types.json"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch, provide } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
ArrowBigUpDashIcon,
|
||||
ChangeSkinIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
HomeIcon,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
SettingsIcon,
|
||||
WorldIcon,
|
||||
XIcon,
|
||||
NewspaperIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
ButtonStyled,
|
||||
Notifications,
|
||||
OverflowMenu,
|
||||
useRelativeTime,
|
||||
NewsArticleCard,
|
||||
} from '@modrinth/ui'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
@@ -59,17 +61,17 @@ import { renderString } from '@modrinth/utils'
|
||||
import { useFetch } from '@/helpers/fetch.js'
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
||||
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
||||
import { get_user } from '@/helpers/cache.js'
|
||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -177,6 +179,7 @@ async function setupApp() {
|
||||
'criticalAnnouncements',
|
||||
true,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
if (res && res.header && res.body) {
|
||||
criticalErrorMessage.value = res
|
||||
@@ -188,15 +191,35 @@ async function setupApp() {
|
||||
)
|
||||
})
|
||||
|
||||
useFetch(`https://modrinth.com/blog/news.json`, 'news', true).then((res) => {
|
||||
if (res && res.articles) {
|
||||
news.value = res.articles
|
||||
}
|
||||
})
|
||||
useFetch(`https://modrinth.com/news/feed/articles.json`, 'news', true)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
if (res && res.articles) {
|
||||
// Format expected by NewsArticleCard component.
|
||||
news.value = res.articles
|
||||
.map((article) => ({
|
||||
...article,
|
||||
path: article.link,
|
||||
thumbnail: article.thumbnail,
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
date: article.date,
|
||||
}))
|
||||
.slice(0, 4)
|
||||
}
|
||||
})
|
||||
|
||||
get_opening_command().then(handleCommand)
|
||||
checkUpdates()
|
||||
fetchCredentials()
|
||||
|
||||
try {
|
||||
const skins = (await get_available_skins()) ?? []
|
||||
const capes = (await get_available_capes()) ?? []
|
||||
generateSkinPreviews(skins, capes)
|
||||
} catch (error) {
|
||||
console.warn('Failed to generate skin previews in app setup.', error)
|
||||
}
|
||||
}
|
||||
|
||||
const stateFailed = ref(false)
|
||||
@@ -241,6 +264,8 @@ const incompatibilityWarningModal = ref()
|
||||
|
||||
const credentials = ref()
|
||||
|
||||
const modrinthLoginFlowWaitModal = ref()
|
||||
|
||||
async function fetchCredentials() {
|
||||
const creds = await getCreds().catch(handleError)
|
||||
if (creds && creds.user_id) {
|
||||
@@ -250,8 +275,24 @@ async function fetchCredentials() {
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
await login().catch(handleError)
|
||||
await fetchCredentials()
|
||||
modrinthLoginFlowWaitModal.value.show()
|
||||
|
||||
try {
|
||||
await login()
|
||||
await fetchCredentials()
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
typeof error['message'] === 'string' &&
|
||||
error.message.includes('Login canceled')
|
||||
) {
|
||||
// Not really an error due to being a result of user interaction, show nothing
|
||||
} else {
|
||||
handleError(error)
|
||||
}
|
||||
} finally {
|
||||
modrinthLoginFlowWaitModal.value.hide()
|
||||
}
|
||||
}
|
||||
|
||||
async function logOut() {
|
||||
@@ -304,6 +345,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const accounts = ref(null)
|
||||
provide('accountsCard', accounts)
|
||||
|
||||
command_listener(handleCommand)
|
||||
async function handleCommand(e) {
|
||||
@@ -379,6 +421,9 @@ function handleAuxClick(e) {
|
||||
<Suspense>
|
||||
<AppSettingsModal ref="settingsModal" />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<AuthGrantFlowWaitModal ref="modrinthLoginFlowWaitModal" @flow-cancel="cancelLogin" />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<InstanceCreationModal ref="installationModal" />
|
||||
</Suspense>
|
||||
@@ -399,6 +444,9 @@ function handleAuxClick(e) {
|
||||
>
|
||||
<CompassIcon />
|
||||
</NavButton>
|
||||
<NavButton v-tooltip.right="'Skins (Beta)'" to="/skins">
|
||||
<ChangeSkinIcon />
|
||||
</NavButton>
|
||||
<NavButton
|
||||
v-tooltip.right="'Library'"
|
||||
to="/library"
|
||||
@@ -459,13 +507,13 @@ function handleAuxClick(e) {
|
||||
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
|
||||
<div class="flex items-center gap-1 ml-3">
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
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"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
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"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
@@ -579,34 +627,20 @@ function handleAuxClick(e) {
|
||||
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||
</suspense>
|
||||
</div>
|
||||
<div v-if="news && news.length > 0" class="pt-4 flex flex-col">
|
||||
<h3 class="px-4 text-lg m-0">News</h3>
|
||||
<template v-for="(item, index) in news" :key="`news-${index}`">
|
||||
<a
|
||||
:class="`flex flex-col outline-offset-[-4px] hover:bg-[--brand-gradient-border] focus:bg-[--brand-gradient-border] px-4 transition-colors ${index === 0 ? 'pt-2 pb-4' : 'py-4'}`"
|
||||
:href="item.link"
|
||||
target="_blank"
|
||||
rel="external"
|
||||
>
|
||||
<img
|
||||
:src="item.thumbnail"
|
||||
alt="News thumbnail"
|
||||
aria-hidden="true"
|
||||
class="w-full aspect-[3/1] object-cover rounded-2xl border-[1px] border-solid border-[--brand-gradient-border]"
|
||||
/>
|
||||
<h4 class="mt-2 mb-0 text-sm leading-none text-contrast font-semibold">
|
||||
{{ item.title }}
|
||||
</h4>
|
||||
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
|
||||
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
|
||||
{{ formatRelativeTime(dayjs(item.date).toISOString()) }}
|
||||
</p>
|
||||
</a>
|
||||
<hr
|
||||
v-if="index !== news.length - 1"
|
||||
class="h-px my-[-2px] mx-4 border-0 m-0 bg-[--brand-gradient-border]"
|
||||
<div v-if="news && news.length > 0" class="pt-4 flex flex-col items-center">
|
||||
<h3 class="px-4 text-lg m-0 text-left w-full">News</h3>
|
||||
<div class="px-4 pt-2 space-y-4 flex flex-col items-center w-full">
|
||||
<NewsArticleCard
|
||||
v-for="(item, index) in news"
|
||||
:key="`news-${index}`"
|
||||
:article="item"
|
||||
/>
|
||||
</template>
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<a href="https://modrinth.com/news" target="_blank" class="my-4">
|
||||
<NewspaperIcon /> View all news
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
BIN
apps/app-frontend/src/assets/skins/herobrine.png
Normal file
BIN
apps/app-frontend/src/assets/skins/herobrine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/app-frontend/src/assets/skins/steve.png
Normal file
BIN
apps/app-frontend/src/assets/skins/steve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -136,7 +136,7 @@ const filteredResults = computed(() => {
|
||||
|
||||
if (sortBy.value === 'Game version') {
|
||||
instances.sort((a, b) => {
|
||||
return a.game_version.localeCompare(b.game_version)
|
||||
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,6 +213,17 @@ const filteredResults = computed(() => {
|
||||
instanceMap.set(entry[0], entry[1])
|
||||
})
|
||||
}
|
||||
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
||||
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
||||
if (group.value === 'Game version') {
|
||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
||||
})
|
||||
instanceMap.clear()
|
||||
sortedEntries.forEach((entry) => {
|
||||
instanceMap.set(entry[0], entry[1])
|
||||
})
|
||||
}
|
||||
|
||||
return instanceMap
|
||||
})
|
||||
|
||||
@@ -9,13 +9,11 @@
|
||||
<Avatar
|
||||
size="36px"
|
||||
:src="
|
||||
selectedAccount
|
||||
? `https://mc-heads.net/avatar/${selectedAccount.id}/128`
|
||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col w-full">
|
||||
<span>{{ selectedAccount ? selectedAccount.username : 'Select account' }}</span>
|
||||
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
||||
<span class="text-secondary text-xs">Minecraft account</span>
|
||||
</div>
|
||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||
@@ -28,28 +26,40 @@
|
||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||
>
|
||||
<div v-if="selectedAccount" class="selected account">
|
||||
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.id}/128`" />
|
||||
<Avatar size="xs" :src="avatarUrl" />
|
||||
<div>
|
||||
<h4>{{ selectedAccount.username }}</h4>
|
||||
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||
<p>Selected</p>
|
||||
</div>
|
||||
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
|
||||
<Button
|
||||
v-tooltip="'Log out'"
|
||||
icon-only
|
||||
color="raised"
|
||||
@click="logout(selectedAccount.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else class="logged-out account">
|
||||
<h4>Not signed in</h4>
|
||||
<Button v-tooltip="'Log in'" icon-only color="primary" @click="login()">
|
||||
<LogInIcon />
|
||||
<Button
|
||||
v-tooltip="'Log in'"
|
||||
:disabled="loginDisabled"
|
||||
icon-only
|
||||
color="primary"
|
||||
@click="login()"
|
||||
>
|
||||
<LogInIcon v-if="!loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||
<div v-for="account in displayAccounts" :key="account.id" class="account-row">
|
||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||
<Button class="option account" @click="setAccount(account)">
|
||||
<Avatar :src="`https://mc-heads.net/avatar/${account.id}/128`" class="icon" />
|
||||
<p>{{ account.username }}</p>
|
||||
<Avatar :src="getAccountAvatarUrl(account)" class="icon" />
|
||||
<p>{{ account.profile.name }}</p>
|
||||
</Button>
|
||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
|
||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -63,7 +73,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon } from '@modrinth/assets'
|
||||
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||
import {
|
||||
@@ -77,6 +87,8 @@ import { handleError } from '@/store/state.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { get_available_skins } from '@/helpers/skins'
|
||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
@@ -89,32 +101,86 @@ defineProps({
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const accounts = ref({})
|
||||
const loginDisabled = ref(false)
|
||||
const defaultUser = ref()
|
||||
const equippedSkin = ref(null)
|
||||
const headUrlCache = ref(new Map())
|
||||
|
||||
async function refreshValues() {
|
||||
defaultUser.value = await get_default_user().catch(handleError)
|
||||
accounts.value = await users().catch(handleError)
|
||||
|
||||
try {
|
||||
const skins = await get_available_skins()
|
||||
equippedSkin.value = skins.find((skin) => skin.is_equipped)
|
||||
|
||||
if (equippedSkin.value) {
|
||||
try {
|
||||
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
|
||||
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
|
||||
} catch (error) {
|
||||
console.warn('Failed to get head render for equipped skin:', error)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
equippedSkin.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginDisabled(value) {
|
||||
loginDisabled.value = value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refreshValues,
|
||||
setLoginDisabled,
|
||||
loginDisabled,
|
||||
})
|
||||
await refreshValues()
|
||||
|
||||
const displayAccounts = computed(() =>
|
||||
accounts.value.filter((account) => defaultUser.value !== account.id),
|
||||
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||
)
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
if (equippedSkin.value?.texture_key) {
|
||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||
if (cachedUrl) {
|
||||
return cachedUrl
|
||||
}
|
||||
return `https://mc-heads.net/avatar/${equippedSkin.value.texture_key}/128`
|
||||
}
|
||||
if (selectedAccount.value?.profile?.id) {
|
||||
return `https://mc-heads.net/avatar/${selectedAccount.value.profile.id}/128`
|
||||
}
|
||||
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
})
|
||||
|
||||
function getAccountAvatarUrl(account) {
|
||||
if (
|
||||
account.profile.id === selectedAccount.value?.profile?.id &&
|
||||
equippedSkin.value?.texture_key
|
||||
) {
|
||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||
if (cachedUrl) {
|
||||
return cachedUrl
|
||||
}
|
||||
}
|
||||
return `https://mc-heads.net/avatar/${account.profile.id}/128`
|
||||
}
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.id === defaultUser.value),
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
|
||||
async function setAccount(account) {
|
||||
defaultUser.value = account.id
|
||||
await set_default_user(account.id).catch(handleError)
|
||||
defaultUser.value = account.profile.id
|
||||
await set_default_user(account.profile.id).catch(handleError)
|
||||
emit('change')
|
||||
}
|
||||
|
||||
async function login() {
|
||||
loginDisabled.value = true
|
||||
const loggedIn = await login_flow().catch(handleSevereError)
|
||||
|
||||
if (loggedIn) {
|
||||
@@ -123,6 +189,7 @@ async function login() {
|
||||
}
|
||||
|
||||
trackEvent('AccountLogIn')
|
||||
loginDisabled.value = false
|
||||
}
|
||||
|
||||
const logout = async (id) => {
|
||||
|
||||
@@ -92,7 +92,7 @@ async function loginMinecraft() {
|
||||
const loggedIn = await login_flow()
|
||||
|
||||
if (loggedIn) {
|
||||
await set_default_user(loggedIn.id).catch(handleError)
|
||||
await set_default_user(loggedIn.profile.id).catch(handleError)
|
||||
}
|
||||
|
||||
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
||||
@@ -219,8 +219,8 @@ async function copyToClipboard(text) {
|
||||
<template v-else-if="metadata.notEnoughSpace">
|
||||
<h3>Not enough space</h3>
|
||||
<p>
|
||||
It looks like there is not enough space on the disk containing the dirctory you
|
||||
selected Please free up some space and try again or cancel the directory change.
|
||||
It looks like there is not enough space on the disk containing the directory you
|
||||
selected. Please free up some space and try again or cancel the directory change.
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -19,7 +19,6 @@ import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import dayjs from 'dayjs'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
@@ -173,7 +172,10 @@ onUnmounted(() => unlisten())
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||
<TimerIcon />
|
||||
<span class="text-sm">
|
||||
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
||||
<template v-if="instance.last_played">
|
||||
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
||||
</template>
|
||||
<template v-else> Never played </template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,8 +239,8 @@ onUnmounted(() => unlisten())
|
||||
</p>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold mt-auto">
|
||||
<GameIcon class="shrink-0" />
|
||||
<span class="text-sm">
|
||||
{{ formatCategory(instance.loader) }} {{ instance.game_version }}
|
||||
<span class="text-sm capitalize">
|
||||
{{ instance.loader }} {{ instance.game_version }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -305,12 +305,16 @@ const [
|
||||
get_game_versions().then(shallowRef).catch(handleError),
|
||||
get_loaders()
|
||||
.then((value) =>
|
||||
value
|
||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
||||
.map((item) => item.name.toLowerCase()),
|
||||
ref(
|
||||
value
|
||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
||||
.map((item) => item.name.toLowerCase()),
|
||||
),
|
||||
)
|
||||
.then(ref)
|
||||
.catch(handleError),
|
||||
.catch((err) => {
|
||||
handleError(err)
|
||||
return ref([])
|
||||
}),
|
||||
])
|
||||
loaders.value.unshift('vanilla')
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ async function testJava() {
|
||||
testingJava.value = true
|
||||
testingJavaSuccess.value = await test_jre(
|
||||
props.modelValue ? props.modelValue.path : '',
|
||||
1,
|
||||
props.version,
|
||||
)
|
||||
testingJava.value = false
|
||||
@@ -127,7 +126,7 @@ async function handleJavaFileInput() {
|
||||
const filePath = await open()
|
||||
|
||||
if (filePath) {
|
||||
let result = await get_jre(filePath.path ?? filePath)
|
||||
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
||||
if (!result) {
|
||||
result = {
|
||||
path: filePath.path ?? filePath,
|
||||
|
||||
@@ -32,8 +32,33 @@ function updateAdPosition() {
|
||||
|
||||
<template>
|
||||
<div ref="adsWrapper" class="ad-parent relative flex w-full justify-center cursor-pointer bg-bg">
|
||||
<div class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 p-6">
|
||||
<p class="m-0 text-2xl font-bold text-contrast">75% of ad revenue goes to creators</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://modrinth.gg?from=app-placeholder"
|
||||
target="_blank"
|
||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
|
||||
>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-light.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="hidden light-image rounded-[inherit]"
|
||||
/>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-dark.webp"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="dark-image rounded-[inherit]"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.light,
|
||||
.light-mode {
|
||||
.dark-image {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.light-image {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,7 @@ const getInstances = async () => {
|
||||
|
||||
return dateB - dateA
|
||||
})
|
||||
.slice(0, 4)
|
||||
.slice(0, 3)
|
||||
}
|
||||
|
||||
await getInstances()
|
||||
|
||||
@@ -25,9 +25,8 @@ const editProfileObject = computed(() => {
|
||||
hooks?: Hooks
|
||||
} = {}
|
||||
|
||||
if (overrideHooks.value) {
|
||||
editProfile.hooks = hooks.value
|
||||
}
|
||||
// When hooks are not overridden per-instance, we want to clear them
|
||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
@@ -6,9 +6,9 @@ import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -34,7 +34,7 @@ const envVars = ref(
|
||||
|
||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
||||
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
||||
const { maxMemory, snapPoints } = await useMemorySlider()
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
@@ -156,6 +156,8 @@ const messages = defineMessages({
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ShieldIcon,
|
||||
SettingsIcon,
|
||||
GaugeIcon,
|
||||
PaintBrushIcon,
|
||||
PaintbrushIcon,
|
||||
GameIcon,
|
||||
CoffeeIcon,
|
||||
} from '@modrinth/assets'
|
||||
@@ -41,7 +41,7 @@ const tabs = [
|
||||
id: 'app.settings.tabs.appearance',
|
||||
defaultMessage: 'Appearance',
|
||||
}),
|
||||
icon: PaintBrushIcon,
|
||||
icon: PaintbrushIcon,
|
||||
content: AppearanceSettings,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { ref } from 'vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
defineProps({
|
||||
onFlowCancel: {
|
||||
type: Function,
|
||||
default() {
|
||||
return async () => {}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const modal = ref()
|
||||
|
||||
function show() {
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value.hide()
|
||||
}
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
||||
<template #title>
|
||||
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
||||
<LogInIcon /> Sign in
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div class="flex justify-center gap-2">
|
||||
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
||||
</div>
|
||||
<p class="text-sm text-secondary">
|
||||
Please sign in at the browser window that just opened to continue.
|
||||
</p>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { NewModal as Modal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
@@ -26,16 +26,16 @@ const props = defineProps({
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
const modal = ref(null)
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
defineExpose({
|
||||
show: () => {
|
||||
show: (e: MouseEvent) => {
|
||||
hide_ads_window()
|
||||
modal.value.show()
|
||||
modal.value?.show(e)
|
||||
},
|
||||
hide: () => {
|
||||
onModalHide()
|
||||
modal.value.hide()
|
||||
modal.value?.hide()
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -56,9 +56,17 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
||||
</div>
|
||||
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
||||
</div>
|
||||
|
||||
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native Decorations</h2>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
|
||||
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
|
||||
</div>
|
||||
<Toggle id="native-decorations" v-model="settings.native_decorations" />
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { Slider, Toggle } from '@modrinth/ui'
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
|
||||
const fetchSettings = await get()
|
||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||
@@ -11,7 +10,7 @@ fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).jo
|
||||
|
||||
const settings = ref(fetchSettings)
|
||||
|
||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||
const { maxMemory, snapPoints } = await useMemorySlider()
|
||||
|
||||
watch(
|
||||
settings,
|
||||
@@ -107,6 +106,8 @@ watch(
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
|
||||
|
||||
413
apps/app-frontend/src/components/ui/skin/EditSkinModal.vue
Normal file
413
apps/app-frontend/src/components/ui/skin/EditSkinModal.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<UploadSkinModal ref="uploadModal" />
|
||||
<ModalWrapper ref="modal" @on-hide="resetState">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast">
|
||||
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin || ''"
|
||||
:cape-src="selectedCapeTexture"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Texture</h2>
|
||||
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Arm style</h2>
|
||||
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
|
||||
<template #default="{ item }">
|
||||
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
|
||||
</template>
|
||||
</RadioButtons>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="text-base font-semibold mb-2">Cape</h2>
|
||||
<div class="flex gap-2">
|
||||
<CapeButton
|
||||
v-if="defaultCape"
|
||||
:id="defaultCape.id"
|
||||
:texture="defaultCape.texture"
|
||||
:name="undefined"
|
||||
:selected="!selectedCape"
|
||||
faded
|
||||
@select="selectCape(undefined)"
|
||||
>
|
||||
<span>Use default cape</span>
|
||||
</CapeButton>
|
||||
<CapeLikeTextButton v-else :highlighted="!selectedCape" @click="selectCape(undefined)">
|
||||
<span>Use default cape</span>
|
||||
</CapeLikeTextButton>
|
||||
|
||||
<CapeButton
|
||||
v-for="cape in visibleCapeList"
|
||||
:id="cape.id"
|
||||
:key="cape.id"
|
||||
:texture="cape.texture"
|
||||
:name="cape.name || 'Cape'"
|
||||
:selected="selectedCape?.id === cape.id"
|
||||
@select="selectCape(cape)"
|
||||
/>
|
||||
|
||||
<CapeLikeTextButton
|
||||
v-if="(capes?.length ?? 0) > 2"
|
||||
tooltip="View more capes"
|
||||
@mouseup="openSelectCapeModal"
|
||||
>
|
||||
<template #icon><ChevronRightIcon /></template>
|
||||
<span>More</span>
|
||||
</CapeLikeTextButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-12">
|
||||
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
||||
<button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
<SelectCapeModal
|
||||
ref="selectCapeModal"
|
||||
:capes="capes || []"
|
||||
@select="handleCapeSelected"
|
||||
@cancel="handleCapeCancel"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, useTemplateRef } from 'vue'
|
||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||
import {
|
||||
SkinPreviewRenderer,
|
||||
Button,
|
||||
RadioButtons,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
ButtonStyled,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
add_and_equip_custom_skin,
|
||||
remove_custom_skin,
|
||||
unequip_skin,
|
||||
type Skin,
|
||||
type Cape,
|
||||
type SkinModel,
|
||||
get_normalized_skin_texture,
|
||||
determineModelType,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import {
|
||||
UploadIcon,
|
||||
CheckIcon,
|
||||
SaveIcon,
|
||||
XIcon,
|
||||
ChevronRightIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
const mode = ref<'new' | 'edit'>('new')
|
||||
const currentSkin = ref<Skin | null>(null)
|
||||
const shouldRestoreModal = ref(false)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const uploadedTextureUrl = ref<string | null>(null)
|
||||
const previewSkin = ref<string>('')
|
||||
|
||||
const variant = ref<SkinModel>('CLASSIC')
|
||||
const selectedCape = ref<Cape | undefined>(undefined)
|
||||
const props = defineProps<{ capes?: Cape[]; defaultCape?: Cape }>()
|
||||
|
||||
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
|
||||
const visibleCapeList = ref<Cape[]>([])
|
||||
|
||||
const sortedCapes = computed(() => {
|
||||
return [...(props.capes || [])].sort((a, b) => {
|
||||
const nameA = (a.name || '').toLowerCase()
|
||||
const nameB = (b.name || '').toLowerCase()
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
})
|
||||
|
||||
function initVisibleCapeList() {
|
||||
if (!props.capes || props.capes.length === 0) {
|
||||
visibleCapeList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (visibleCapeList.value.length === 0) {
|
||||
if (selectedCape.value) {
|
||||
const otherCape = getSortedCapeExcluding(selectedCape.value.id)
|
||||
visibleCapeList.value = otherCape ? [selectedCape.value, otherCape] : [selectedCape.value]
|
||||
} else {
|
||||
visibleCapeList.value = getSortedCapes(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSortedCapes(count: number): Cape[] {
|
||||
if (!sortedCapes.value || sortedCapes.value.length === 0) return []
|
||||
return sortedCapes.value.slice(0, count)
|
||||
}
|
||||
|
||||
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
|
||||
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
|
||||
return sortedCapes.value.find((cape) => cape.id !== excludeId)
|
||||
}
|
||||
|
||||
async function loadPreviewSkin() {
|
||||
if (uploadedTextureUrl.value) {
|
||||
previewSkin.value = uploadedTextureUrl.value
|
||||
} else if (currentSkin.value) {
|
||||
try {
|
||||
previewSkin.value = await get_normalized_skin_texture(currentSkin.value)
|
||||
} catch (error) {
|
||||
console.error('Failed to load skin texture:', error)
|
||||
previewSkin.value = '/src/assets/skins/steve.png'
|
||||
}
|
||||
} else {
|
||||
previewSkin.value = '/src/assets/skins/steve.png'
|
||||
}
|
||||
}
|
||||
|
||||
const hasEdits = computed(() => {
|
||||
if (mode.value !== 'edit') return true
|
||||
if (uploadedTextureUrl.value) return true
|
||||
if (!currentSkin.value) return false
|
||||
if (variant.value !== currentSkin.value.variant) return true
|
||||
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const disableSave = computed(
|
||||
() =>
|
||||
(mode.value === 'new' && !uploadedTextureUrl.value) ||
|
||||
(mode.value === 'edit' && !hasEdits.value),
|
||||
)
|
||||
|
||||
const saveTooltip = computed(() => {
|
||||
if (isSaving.value) return 'Saving...'
|
||||
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
||||
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
||||
return undefined
|
||||
})
|
||||
|
||||
function resetState() {
|
||||
mode.value = 'new'
|
||||
currentSkin.value = null
|
||||
uploadedTextureUrl.value = null
|
||||
previewSkin.value = ''
|
||||
variant.value = 'CLASSIC'
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
shouldRestoreModal.value = false
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
async function show(e: MouseEvent, skin?: Skin) {
|
||||
mode.value = skin ? 'edit' : 'new'
|
||||
currentSkin.value = skin ?? null
|
||||
if (skin) {
|
||||
variant.value = skin.variant
|
||||
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
|
||||
} else {
|
||||
variant.value = 'CLASSIC'
|
||||
selectedCape.value = undefined
|
||||
}
|
||||
visibleCapeList.value = []
|
||||
initVisibleCapeList()
|
||||
|
||||
await loadPreviewSkin()
|
||||
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
||||
mode.value = 'new'
|
||||
currentSkin.value = null
|
||||
uploadedTextureUrl.value = skinTextureUrl
|
||||
variant.value = await determineModelType(skinTextureUrl)
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
initVisibleCapeList()
|
||||
|
||||
await loadPreviewSkin()
|
||||
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
async function restoreWithNewTexture(skinTextureUrl: string) {
|
||||
uploadedTextureUrl.value = skinTextureUrl
|
||||
await loadPreviewSkin()
|
||||
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
setTimeout(() => resetState(), 250)
|
||||
}
|
||||
|
||||
function selectCape(cape: Cape | undefined) {
|
||||
if (cape && selectedCape.value?.id !== cape.id) {
|
||||
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
|
||||
if (!isInVisibleList && visibleCapeList.value.length > 0) {
|
||||
visibleCapeList.value.splice(0, 1, cape)
|
||||
|
||||
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
|
||||
const otherCape = getSortedCapeExcluding(cape.id)
|
||||
if (otherCape) {
|
||||
visibleCapeList.value.splice(1, 1, otherCape)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selectedCape.value = cape
|
||||
}
|
||||
|
||||
function handleCapeSelected(cape: Cape | undefined) {
|
||||
selectCape(cape)
|
||||
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCapeCancel() {
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
modal.value?.show()
|
||||
shouldRestoreModal.value = false
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function openSelectCapeModal(e: MouseEvent) {
|
||||
if (!selectCapeModal.value) return
|
||||
|
||||
shouldRestoreModal.value = true
|
||||
modal.value?.hide()
|
||||
|
||||
setTimeout(() => {
|
||||
selectCapeModal.value?.show(
|
||||
e,
|
||||
currentSkin.value?.texture_key,
|
||||
selectedCape.value,
|
||||
previewSkin.value,
|
||||
variant.value,
|
||||
)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function openUploadSkinModal(e: MouseEvent) {
|
||||
shouldRestoreModal.value = true
|
||||
modal.value?.hide()
|
||||
emit('open-upload-modal', e)
|
||||
}
|
||||
|
||||
function restoreModal() {
|
||||
if (shouldRestoreModal.value) {
|
||||
setTimeout(() => {
|
||||
const fakeEvent = new MouseEvent('click')
|
||||
modal.value?.show(fakeEvent)
|
||||
shouldRestoreModal.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
isSaving.value = true
|
||||
|
||||
try {
|
||||
let textureUrl: string
|
||||
|
||||
if (uploadedTextureUrl.value) {
|
||||
textureUrl = uploadedTextureUrl.value
|
||||
} else {
|
||||
textureUrl = currentSkin.value!.texture
|
||||
}
|
||||
|
||||
await unequip_skin()
|
||||
|
||||
const bytes: Uint8Array = new Uint8Array(await (await fetch(textureUrl)).arrayBuffer())
|
||||
|
||||
if (mode.value === 'new') {
|
||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||
emit('saved')
|
||||
} else {
|
||||
await add_and_equip_custom_skin(bytes, variant.value, selectedCape.value)
|
||||
await remove_custom_skin(currentSkin.value!)
|
||||
emit('saved')
|
||||
}
|
||||
|
||||
hide()
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch([uploadedTextureUrl, currentSkin], async () => {
|
||||
await loadPreviewSkin()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.capes,
|
||||
() => {
|
||||
initVisibleCapeList()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'saved'): void
|
||||
(event: 'deleted', skin: Skin): void
|
||||
(event: 'open-upload-modal', mouseEvent: MouseEvent): void
|
||||
}>()
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
showNew,
|
||||
restoreWithNewTexture,
|
||||
hide,
|
||||
shouldRestoreModal,
|
||||
restoreModal,
|
||||
})
|
||||
</script>
|
||||
140
apps/app-frontend/src/components/ui/skin/SelectCapeModal.vue
Normal file
140
apps/app-frontend/src/components/ui/skin/SelectCapeModal.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef, ref, computed } from 'vue'
|
||||
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
||||
import {
|
||||
ButtonStyled,
|
||||
ScrollablePanel,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
SkinPreviewRenderer,
|
||||
} from '@modrinth/ui'
|
||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', cape: Cape | undefined): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
capes: Cape[]
|
||||
}>()
|
||||
|
||||
const sortedCapes = computed(() => {
|
||||
return [...props.capes].sort((a, b) => {
|
||||
const nameA = (a.name || '').toLowerCase()
|
||||
const nameB = (b.name || '').toLowerCase()
|
||||
return nameA.localeCompare(nameB)
|
||||
})
|
||||
})
|
||||
|
||||
const currentSkinId = ref<string | undefined>()
|
||||
const currentSkinTexture = ref<string | undefined>()
|
||||
const currentSkinVariant = ref<SkinModel>('CLASSIC')
|
||||
const currentCapeTexture = computed<string | undefined>(() => currentCape.value?.texture)
|
||||
const currentCape = ref<Cape | undefined>()
|
||||
|
||||
function show(
|
||||
e: MouseEvent,
|
||||
skinId?: string,
|
||||
selected?: Cape,
|
||||
skinTexture?: string,
|
||||
variant?: SkinModel,
|
||||
) {
|
||||
currentSkinId.value = skinId
|
||||
currentSkinTexture.value = skinTexture
|
||||
currentSkinVariant.value = variant || 'CLASSIC'
|
||||
currentCape.value = selected
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
function select() {
|
||||
emit('select', currentCape.value)
|
||||
hide()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
function updateSelectedCape(cape: Cape | undefined) {
|
||||
currentCape.value = cape
|
||||
}
|
||||
|
||||
function onModalHide() {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<ModalWrapper ref="modal" @on-hide="onModalHide">
|
||||
<template #title>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-lg font-extrabold text-heading">Change cape</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col md:flex-row gap-6">
|
||||
<div class="max-h-[25rem] h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
v-if="currentSkinTexture"
|
||||
:cape-src="currentCapeTexture"
|
||||
:texture-src="currentSkinTexture"
|
||||
:variant="currentSkinVariant"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
:initial-rotation="Math.PI + Math.PI / 8"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full my-auto">
|
||||
<ScrollablePanel class="max-h-[20rem] max-w-[30rem] mb-5 h-full">
|
||||
<div class="flex flex-wrap gap-2 justify-center content-start overflow-y-auto h-full">
|
||||
<CapeLikeTextButton
|
||||
tooltip="No Cape"
|
||||
:highlighted="!currentCape"
|
||||
@click="updateSelectedCape(undefined)"
|
||||
>
|
||||
<template #icon>
|
||||
<XIcon />
|
||||
</template>
|
||||
<span>None</span>
|
||||
</CapeLikeTextButton>
|
||||
<CapeButton
|
||||
v-for="cape in sortedCapes"
|
||||
:id="cape.id"
|
||||
:key="cape.id"
|
||||
:name="cape.name"
|
||||
:texture="cape.texture"
|
||||
:selected="currentCape?.id === cape.id"
|
||||
@select="updateSelectedCape(cape)"
|
||||
/>
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="select">
|
||||
<CheckIcon />
|
||||
Select
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="hide">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
140
apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue
Normal file
140
apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<ModalWrapper ref="modal" @on-hide="hide(true)">
|
||||
<template #title>
|
||||
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
|
||||
</template>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
|
||||
@click="triggerFileInput"
|
||||
>
|
||||
<p class="mx-auto mb-0 text-primary font-bold text-lg text-center flex items-center gap-2">
|
||||
<UploadIcon /> Select skin texture file
|
||||
</p>
|
||||
<p class="mx-auto mt-0 text-secondary text-sm text-center">
|
||||
Drag and drop or click here to browse
|
||||
</p>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept="image/png"
|
||||
class="hidden"
|
||||
@change="handleInputFileChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeUnmount, watch } from 'vue'
|
||||
import { UploadIcon } from '@modrinth/assets'
|
||||
import { useNotifications } from '@/store/state'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get_dragged_skin_data } from '@/helpers/skins'
|
||||
|
||||
const notifications = useNotifications()
|
||||
|
||||
const modal = ref()
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
const unlisten = ref<() => void>()
|
||||
const modalVisible = ref(false)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'uploaded', data: ArrayBuffer): void
|
||||
(e: 'canceled'): void
|
||||
}>()
|
||||
|
||||
function show(e?: MouseEvent) {
|
||||
modal.value?.show(e)
|
||||
modalVisible.value = true
|
||||
setupDragDropListener()
|
||||
}
|
||||
|
||||
function hide(emitCanceled = false) {
|
||||
modal.value?.hide()
|
||||
modalVisible.value = false
|
||||
cleanupDragDropListener()
|
||||
resetState()
|
||||
if (emitCanceled) {
|
||||
emit('canceled')
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
}
|
||||
|
||||
function triggerFileInput() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleInputFileChange(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (!files || files.length === 0) {
|
||||
return
|
||||
}
|
||||
const file = files[0]
|
||||
const buffer = await file.arrayBuffer()
|
||||
await processData(buffer)
|
||||
}
|
||||
|
||||
async function setupDragDropListener() {
|
||||
try {
|
||||
if (modalVisible.value) {
|
||||
await cleanupDragDropListener()
|
||||
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
if (event.payload.type !== 'drop') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.payload.paths || event.payload.paths.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const filePath = event.payload.paths[0]
|
||||
|
||||
try {
|
||||
const data = await get_dragged_skin_data(filePath)
|
||||
await processData(data.buffer)
|
||||
} catch (error) {
|
||||
notifications.addNotification({
|
||||
title: 'Error processing file',
|
||||
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set up drag and drop listener:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupDragDropListener() {
|
||||
if (unlisten.value) {
|
||||
unlisten.value()
|
||||
unlisten.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function processData(buffer: ArrayBuffer) {
|
||||
emit('uploaded', buffer)
|
||||
hide()
|
||||
}
|
||||
|
||||
watch(modalVisible, (isVisible) => {
|
||||
if (isVisible) {
|
||||
setupDragDropListener()
|
||||
} else {
|
||||
cleanupDragDropListener()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cleanupDragDropListener()
|
||||
})
|
||||
|
||||
defineExpose({ show, hide })
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
EyeIcon,
|
||||
@@ -42,6 +43,7 @@ const emit = defineEmits<{
|
||||
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
last_played: Dayjs
|
||||
}>()
|
||||
|
||||
const loadingModpack = ref(!!props.instance.linked_data)
|
||||
@@ -147,12 +149,12 @@ onUnmounted(() => {
|
||||
: null
|
||||
"
|
||||
class="w-fit shrink-0"
|
||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': instance.last_played }"
|
||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
|
||||
>
|
||||
<template v-if="instance.last_played">
|
||||
<template v-if="last_played">
|
||||
{{
|
||||
formatMessage(commonMessages.playedLabel, {
|
||||
time: formatRelativeTime(instance.last_played.toISOString()),
|
||||
time: formatRelativeTime(last_played.toISOString?.()),
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type ProtocolVersion,
|
||||
type ServerWorld,
|
||||
type ServerData,
|
||||
type WorldWithProfile,
|
||||
@@ -33,7 +34,7 @@ const theme = useTheming()
|
||||
|
||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||
const serverData = ref<Record<string, ServerData>>({})
|
||||
const protocolVersions = ref<Record<string, number | null>>({})
|
||||
const protocolVersions = ref<Record<string, ProtocolVersion | null>>({})
|
||||
|
||||
const MIN_JUMP_BACK_IN = 3
|
||||
const MAX_JUMP_BACK_IN = 6
|
||||
@@ -84,7 +85,7 @@ async function populateJumpBackIn() {
|
||||
|
||||
worldItems.push({
|
||||
type: 'world',
|
||||
last_played: dayjs(world.last_played),
|
||||
last_played: dayjs(world.last_played ?? 0),
|
||||
world: world,
|
||||
instance: instance,
|
||||
})
|
||||
@@ -121,11 +122,8 @@ async function populateJumpBackIn() {
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
servers.forEach(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,20 +136,20 @@ async function populateJumpBackIn() {
|
||||
|
||||
instanceItems.push({
|
||||
type: 'instance',
|
||||
last_played: dayjs(instance.last_played),
|
||||
last_played: dayjs(instance.last_played ?? 0),
|
||||
instance: instance,
|
||||
})
|
||||
}
|
||||
|
||||
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
||||
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
|
||||
items.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
|
||||
jumpBackInItems.value = items
|
||||
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
||||
.slice(0, MAX_JUMP_BACK_IN)
|
||||
}
|
||||
|
||||
async function refreshServer(address: string, instancePath: string) {
|
||||
await refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||
function refreshServer(address: string, instancePath: string) {
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||
}
|
||||
|
||||
async function joinWorld(world: WorldWithProfile) {
|
||||
@@ -291,7 +289,7 @@ onUnmounted(() => {
|
||||
"
|
||||
@stop="() => stopInstance(item.instance.path)"
|
||||
/>
|
||||
<InstanceItem v-else :instance="item.instance" />
|
||||
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
import type {
|
||||
ProtocolVersion,
|
||||
ServerStatus,
|
||||
ServerWorld,
|
||||
SingleplayerWorld,
|
||||
World,
|
||||
set_world_display_status,
|
||||
getWorldIdentifier,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||
import {
|
||||
useRelativeTime,
|
||||
Avatar,
|
||||
@@ -49,6 +52,7 @@ const router = useRouter()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
(e: 'open-folder', world: SingleplayerWorld): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -58,7 +62,7 @@ const props = withDefaults(
|
||||
playingWorld?: boolean
|
||||
startingInstance?: boolean
|
||||
supportsQuickPlay?: boolean
|
||||
currentProtocol?: number | null
|
||||
currentProtocol?: ProtocolVersion | null
|
||||
highlighted?: boolean
|
||||
|
||||
// Server only
|
||||
@@ -105,23 +109,10 @@ const serverIncompatible = computed(
|
||||
!!props.serverStatus &&
|
||||
!!props.serverStatus.version?.protocol &&
|
||||
!!props.currentProtocol &&
|
||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
||||
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
||||
)
|
||||
|
||||
function getPingLevel(ping: number) {
|
||||
if (ping < 150) {
|
||||
return 5
|
||||
} else if (ping < 300) {
|
||||
return 4
|
||||
} else if (ping < 600) {
|
||||
return 3
|
||||
} else if (ping < 1000) {
|
||||
return 2
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -145,6 +136,14 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.game_already_open',
|
||||
defaultMessage: 'Instance is already open',
|
||||
},
|
||||
noContact: {
|
||||
id: 'instance.worlds.no_contact',
|
||||
defaultMessage: "Server couldn't be contacted",
|
||||
},
|
||||
incompatibleServer: {
|
||||
id: 'instance.worlds.incompatible_server',
|
||||
defaultMessage: 'Server is incompatible',
|
||||
},
|
||||
copyAddress: {
|
||||
id: 'instance.worlds.copy_address',
|
||||
defaultMessage: 'Copy address',
|
||||
@@ -319,39 +318,33 @@ const messages = defineMessages({
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||
<template v-if="world.type === 'singleplayer' || serverStatus">
|
||||
<ButtonStyled
|
||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||
color="red"
|
||||
>
|
||||
<button @click="emit('stop')">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
serverIncompatible
|
||||
? 'Server is incompatible'
|
||||
<ButtonStyled
|
||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||
color="red"
|
||||
>
|
||||
<button @click="emit('stop')">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
!serverStatus
|
||||
? formatMessage(messages.noContact)
|
||||
: serverIncompatible
|
||||
? formatMessage(messages.incompatibleServer)
|
||||
: !supportsQuickPlay
|
||||
? formatMessage(messages.noQuickPlay)
|
||||
: playingOtherWorld || locked
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: null
|
||||
"
|
||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else>
|
||||
<button class="invisible">
|
||||
<PlayIcon aria-hidden="true" />
|
||||
"
|
||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -394,8 +387,7 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'open-folder',
|
||||
shown: world.type === 'singleplayer',
|
||||
action: () =>
|
||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
|
||||
@@ -56,6 +56,7 @@ function show(world: SingleplayerWorld) {
|
||||
icon.value = world.icon
|
||||
displayStatus.value = world.display_status
|
||||
hideFromHome.value = world.display_status === 'hidden'
|
||||
removeIcon.value = false
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
|
||||
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
|
||||
export default async function () {
|
||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||
|
||||
const snapPoints = computed(() => {
|
||||
let points = []
|
||||
let memory = 2048
|
||||
|
||||
while (memory <= maxMemory.value) {
|
||||
points.push(memory)
|
||||
memory *= 2
|
||||
}
|
||||
|
||||
return points
|
||||
})
|
||||
|
||||
return { maxMemory, snapPoints }
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ofetch } from 'ofetch'
|
||||
import { fetch } from '@tauri-apps/plugin-http'
|
||||
import { handleError } from '@/store/state.js'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
|
||||
export const useFetch = async (url, item, isSilent) => {
|
||||
try {
|
||||
const version = await getVersion()
|
||||
|
||||
return await ofetch(url, {
|
||||
return await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'User-Agent': `modrinth/theseus/${version} (support@modrinth.com)` },
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
@@ -36,8 +36,8 @@ export async function get_jre(path) {
|
||||
|
||||
// Tests JRE version by running 'java -version' on it.
|
||||
// Returns true if the version is valid, and matches given (after extraction)
|
||||
export async function test_jre(path, majorVersion, minorVersion) {
|
||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
|
||||
export async function test_jre(path, majorVersion) {
|
||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
|
||||
}
|
||||
|
||||
// Automatically installs specified java version
|
||||
|
||||
@@ -16,3 +16,7 @@ export async function logout() {
|
||||
export async function get() {
|
||||
return await invoke('plugin:mr-auth|get')
|
||||
}
|
||||
|
||||
export async function cancelLogin() {
|
||||
return await invoke('plugin:mr-auth|cancel_modrinth_login')
|
||||
}
|
||||
|
||||
446
apps/app-frontend/src/helpers/rendering/batch-skin-renderer.ts
Normal file
446
apps/app-frontend/src/helpers/rendering/batch-skin-renderer.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
import * as THREE from 'three'
|
||||
import type { Skin, Cape } from '../skins'
|
||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
||||
import { reactive } from 'vue'
|
||||
import {
|
||||
setupSkinModel,
|
||||
disposeCaches,
|
||||
loadTexture,
|
||||
applyCapeTexture,
|
||||
createTransparentTexture,
|
||||
} from '@modrinth/utils'
|
||||
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||
import { headStorage } from '../storage/head-storage'
|
||||
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
|
||||
export interface RenderResult {
|
||||
forwards: string
|
||||
backwards: string
|
||||
}
|
||||
|
||||
export interface RawRenderResult {
|
||||
forwards: Blob
|
||||
backwards: Blob
|
||||
}
|
||||
|
||||
class BatchSkinRenderer {
|
||||
private renderer: THREE.WebGLRenderer | null = null
|
||||
private scene: THREE.Scene | null = null
|
||||
private camera: THREE.PerspectiveCamera | null = null
|
||||
private currentModel: THREE.Group | null = null
|
||||
private readonly width: number
|
||||
private readonly height: number
|
||||
|
||||
constructor(width: number = 360, height: number = 504) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
|
||||
private initializeRenderer(): void {
|
||||
if (this.renderer) return
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = this.width
|
||||
canvas.height = this.height
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
preserveDrawingBuffer: true,
|
||||
})
|
||||
|
||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
this.renderer.toneMappingExposure = 10.0
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.setSize(this.width, this.height)
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.position.set(2, 4, 3)
|
||||
this.scene.add(ambientLight)
|
||||
this.scene.add(directionalLight)
|
||||
}
|
||||
|
||||
public async renderSkin(
|
||||
textureUrl: string,
|
||||
modelUrl: string,
|
||||
capeUrl?: string,
|
||||
): Promise<RawRenderResult> {
|
||||
this.initializeRenderer()
|
||||
|
||||
this.clearScene()
|
||||
|
||||
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
||||
|
||||
const headPart = this.currentModel!.getObjectByName('Head')
|
||||
let lookAtTarget: [number, number, number]
|
||||
|
||||
if (headPart) {
|
||||
const headPosition = new THREE.Vector3()
|
||||
headPart.getWorldPosition(headPosition)
|
||||
lookAtTarget = [headPosition.x, headPosition.y - 0.3, headPosition.z]
|
||||
} else {
|
||||
throw new Error("Failed to find 'Head' object in model.")
|
||||
}
|
||||
|
||||
const frontCameraPos: [number, number, number] = [-1.3, 1, 6.3]
|
||||
const backCameraPos: [number, number, number] = [-1.3, 1, -2.5]
|
||||
|
||||
const forwards = await this.renderView(frontCameraPos, lookAtTarget)
|
||||
const backwards = await this.renderView(backCameraPos, lookAtTarget)
|
||||
|
||||
return { forwards, backwards }
|
||||
}
|
||||
|
||||
private async renderView(
|
||||
cameraPosition: [number, number, number],
|
||||
lookAtPosition: [number, number, number],
|
||||
): Promise<Blob> {
|
||||
if (!this.camera || !this.renderer || !this.scene) {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
this.camera.position.set(...cameraPosition)
|
||||
this.camera.lookAt(...lookAtPosition)
|
||||
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
|
||||
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
||||
const response = await fetch(dataUrl)
|
||||
return await response.blob()
|
||||
}
|
||||
|
||||
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||
if (!this.scene) {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl)
|
||||
|
||||
if (capeUrl) {
|
||||
const capeTexture = await loadTexture(capeUrl)
|
||||
applyCapeTexture(model, capeTexture)
|
||||
} else {
|
||||
const transparentTexture = createTransparentTexture()
|
||||
applyCapeTexture(model, null, transparentTexture)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.add(model)
|
||||
group.position.set(0, 0.3, 1.95)
|
||||
group.scale.set(0.8, 0.8, 0.8)
|
||||
|
||||
this.scene.add(group)
|
||||
this.currentModel = group
|
||||
}
|
||||
|
||||
private clearScene(): void {
|
||||
if (!this.scene) return
|
||||
|
||||
while (this.scene.children.length > 0) {
|
||||
const child = this.scene.children[0]
|
||||
this.scene.remove(child)
|
||||
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.geometry) child.geometry.dispose()
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((material) => material.dispose())
|
||||
} else {
|
||||
child.material.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.position.set(2, 4, 3)
|
||||
this.scene.add(ambientLight)
|
||||
this.scene.add(directionalLight)
|
||||
|
||||
this.currentModel = null
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose()
|
||||
}
|
||||
disposeCaches()
|
||||
}
|
||||
}
|
||||
|
||||
function getModelUrlForVariant(variant: string): string {
|
||||
switch (variant) {
|
||||
case 'SLIM':
|
||||
return SlimPlayerModel
|
||||
case 'CLASSIC':
|
||||
case 'UNKNOWN':
|
||||
default:
|
||||
return ClassicPlayerModel
|
||||
}
|
||||
}
|
||||
|
||||
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||
export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||
const DEBUG_MODE = false
|
||||
|
||||
let sharedRenderer: BatchSkinRenderer | null = null
|
||||
function getSharedRenderer(): BatchSkinRenderer {
|
||||
if (!sharedRenderer) {
|
||||
sharedRenderer = new BatchSkinRenderer()
|
||||
}
|
||||
return sharedRenderer
|
||||
}
|
||||
|
||||
export function disposeSharedRenderer(): void {
|
||||
if (sharedRenderer) {
|
||||
sharedRenderer.dispose()
|
||||
sharedRenderer = null
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
const validKeys = new Set<string>()
|
||||
const validHeadKeys = new Set<string>()
|
||||
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
validKeys.add(key)
|
||||
validHeadKeys.add(headKey)
|
||||
}
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||
await headStorage.cleanupInvalidKeys(validHeadKeys)
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup unused skin previews:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const sourceCanvas = document.createElement('canvas')
|
||||
const sourceCtx = sourceCanvas.getContext('2d')
|
||||
|
||||
if (!sourceCtx) {
|
||||
throw new Error('Could not get 2D context from source canvas')
|
||||
}
|
||||
|
||||
sourceCanvas.width = img.width
|
||||
sourceCanvas.height = img.height
|
||||
|
||||
sourceCtx.drawImage(img, 0, 0)
|
||||
|
||||
const outputCanvas = document.createElement('canvas')
|
||||
const outputCtx = outputCanvas.getContext('2d')
|
||||
|
||||
if (!outputCtx) {
|
||||
throw new Error('Could not get 2D context from output canvas')
|
||||
}
|
||||
|
||||
outputCanvas.width = size
|
||||
outputCanvas.height = size
|
||||
|
||||
outputCtx.imageSmoothingEnabled = false
|
||||
|
||||
const headImageData = sourceCtx.getImageData(8, 8, 8, 8)
|
||||
|
||||
const headCanvas = document.createElement('canvas')
|
||||
const headCtx = headCanvas.getContext('2d')
|
||||
|
||||
if (!headCtx) {
|
||||
throw new Error('Could not get 2D context from head canvas')
|
||||
}
|
||||
|
||||
headCanvas.width = 8
|
||||
headCanvas.height = 8
|
||||
headCtx.putImageData(headImageData, 0, 0)
|
||||
|
||||
outputCtx.drawImage(headCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||
|
||||
const hatImageData = sourceCtx.getImageData(40, 8, 8, 8)
|
||||
|
||||
const hatCanvas = document.createElement('canvas')
|
||||
const hatCtx = hatCanvas.getContext('2d')
|
||||
|
||||
if (!hatCtx) {
|
||||
throw new Error('Could not get 2D context from hat canvas')
|
||||
}
|
||||
|
||||
hatCanvas.width = 8
|
||||
hatCanvas.height = 8
|
||||
hatCtx.putImageData(hatImageData, 0, 0)
|
||||
|
||||
const hatPixels = hatImageData.data
|
||||
let hasHat = false
|
||||
|
||||
for (let i = 3; i < hatPixels.length; i += 4) {
|
||||
if (hatPixels[i] > 0) {
|
||||
hasHat = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHat) {
|
||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||
}
|
||||
|
||||
outputCanvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
},
|
||||
'image/webp',
|
||||
0.9,
|
||||
)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Failed to load skin texture image'))
|
||||
}
|
||||
|
||||
img.src = skinUrl
|
||||
})
|
||||
}
|
||||
|
||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
|
||||
if (headBlobUrlMap.has(headKey)) {
|
||||
if (DEBUG_MODE) {
|
||||
const url = headBlobUrlMap.get(headKey)!
|
||||
URL.revokeObjectURL(url)
|
||||
headBlobUrlMap.delete(headKey)
|
||||
} else {
|
||||
return headBlobUrlMap.get(headKey)!
|
||||
}
|
||||
}
|
||||
|
||||
const skinUrl = await get_normalized_skin_texture(skin)
|
||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||
const headUrl = URL.createObjectURL(headBlob)
|
||||
|
||||
headBlobUrlMap.set(headKey, headUrl)
|
||||
|
||||
try {
|
||||
await headStorage.store(headKey, headBlob)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store head render in persistent storage:', error)
|
||||
}
|
||||
|
||||
return headUrl
|
||||
}
|
||||
|
||||
export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
||||
return await generateHeadRender(skin)
|
||||
}
|
||||
|
||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
try {
|
||||
const skinKeys = skins.map(
|
||||
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
||||
)
|
||||
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
|
||||
|
||||
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
|
||||
skinPreviewStorage.batchRetrieve(skinKeys),
|
||||
headStorage.batchRetrieve(headKeys),
|
||||
])
|
||||
|
||||
for (let i = 0; i < skins.length; i++) {
|
||||
const skinKey = skinKeys[i]
|
||||
const headKey = headKeys[i]
|
||||
|
||||
const rawCached = cachedSkinPreviews[skinKey]
|
||||
if (rawCached) {
|
||||
const cached: RenderResult = {
|
||||
forwards: URL.createObjectURL(rawCached.forwards),
|
||||
backwards: URL.createObjectURL(rawCached.backwards),
|
||||
}
|
||||
skinBlobUrlMap.set(skinKey, cached)
|
||||
}
|
||||
|
||||
const cachedHead = cachedHeadPreviews[headKey]
|
||||
if (cachedHead) {
|
||||
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
|
||||
}
|
||||
}
|
||||
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
|
||||
if (skinBlobUrlMap.has(key)) {
|
||||
if (DEBUG_MODE) {
|
||||
const result = skinBlobUrlMap.get(key)!
|
||||
URL.revokeObjectURL(result.forwards)
|
||||
URL.revokeObjectURL(result.backwards)
|
||||
skinBlobUrlMap.delete(key)
|
||||
} else continue
|
||||
}
|
||||
|
||||
const renderer = getSharedRenderer()
|
||||
|
||||
let variant = skin.variant
|
||||
if (variant === 'UNKNOWN') {
|
||||
try {
|
||||
variant = await determineModelType(skin.texture)
|
||||
} catch (error) {
|
||||
console.error(`Failed to determine model type for skin ${key}:`, error)
|
||||
variant = 'CLASSIC'
|
||||
}
|
||||
}
|
||||
|
||||
const modelUrl = getModelUrlForVariant(variant)
|
||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
||||
const rawRenderResult = await renderer.renderSkin(
|
||||
await get_normalized_skin_texture(skin),
|
||||
modelUrl,
|
||||
cape?.texture,
|
||||
)
|
||||
|
||||
const renderResult: RenderResult = {
|
||||
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
||||
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
||||
}
|
||||
|
||||
skinBlobUrlMap.set(key, renderResult)
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.store(key, rawRenderResult)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store skin preview in persistent storage:', error)
|
||||
}
|
||||
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
if (!headBlobUrlMap.has(headKey)) {
|
||||
await generateHeadRender(skin)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
disposeSharedRenderer()
|
||||
await cleanupUnusedPreviews(skins)
|
||||
|
||||
await skinPreviewStorage.debugCalculateStorage()
|
||||
await headStorage.debugCalculateStorage()
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export type AppSettings = {
|
||||
theme: ColorTheme
|
||||
default_page: 'home' | 'library'
|
||||
collapsed_navigation: boolean
|
||||
hide_nametag_skins_page: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
toggle_sidebar: boolean
|
||||
|
||||
164
apps/app-frontend/src/helpers/skins.ts
Normal file
164
apps/app-frontend/src/helpers/skins.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
|
||||
export interface Cape {
|
||||
id: string
|
||||
name: string
|
||||
texture: string
|
||||
is_default: boolean
|
||||
is_equipped: boolean
|
||||
}
|
||||
|
||||
export type SkinModel = 'CLASSIC' | 'SLIM' | 'UNKNOWN'
|
||||
export type SkinSource = 'default' | 'custom_external' | 'custom'
|
||||
|
||||
export interface Skin {
|
||||
texture_key: string
|
||||
name?: string
|
||||
variant: SkinModel
|
||||
cape_id?: string
|
||||
texture: string
|
||||
source: SkinSource
|
||||
is_equipped: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_MODEL_SORTING = ['Steve', 'Alex'] as string[]
|
||||
|
||||
export const DEFAULT_MODELS: Record<string, SkinModel> = {
|
||||
Steve: 'CLASSIC',
|
||||
Alex: 'SLIM',
|
||||
Zuri: 'CLASSIC',
|
||||
Sunny: 'CLASSIC',
|
||||
Noor: 'SLIM',
|
||||
Makena: 'SLIM',
|
||||
Kai: 'CLASSIC',
|
||||
Efe: 'SLIM',
|
||||
Ari: 'CLASSIC',
|
||||
}
|
||||
|
||||
export function filterSavedSkins(list: Skin[]) {
|
||||
const customSkins = list.filter((s) => s.source !== 'default')
|
||||
fixUnknownSkins(customSkins).catch(handleError)
|
||||
return customSkins
|
||||
}
|
||||
|
||||
export async function determineModelType(texture: string): Promise<'SLIM' | 'CLASSIC'> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
|
||||
if (!context) {
|
||||
return reject(new Error('Failed to create canvas rendering context.'))
|
||||
}
|
||||
|
||||
const image = new Image()
|
||||
image.crossOrigin = 'anonymous'
|
||||
image.src = texture
|
||||
|
||||
image.onload = () => {
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
|
||||
context.drawImage(image, 0, 0)
|
||||
|
||||
const armX = 54
|
||||
const armY = 20
|
||||
const armWidth = 2
|
||||
const armHeight = 12
|
||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
|
||||
if (imageData[alphaIndex] !== 0) {
|
||||
resolve('CLASSIC')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
canvas.remove()
|
||||
resolve('SLIM')
|
||||
}
|
||||
|
||||
image.onerror = () => {
|
||||
canvas.remove()
|
||||
reject(new Error('Failed to load the image.'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function fixUnknownSkins(list: Skin[]) {
|
||||
const unknownSkins = list.filter((s) => s.variant === 'UNKNOWN')
|
||||
for (const unknownSkin of unknownSkins) {
|
||||
unknownSkin.variant = await determineModelType(unknownSkin.texture)
|
||||
}
|
||||
}
|
||||
|
||||
export function filterDefaultSkins(list: Skin[]) {
|
||||
return list
|
||||
.filter(
|
||||
(s) =>
|
||||
s.source === 'default' &&
|
||||
(!s.name || !(s.name in DEFAULT_MODELS) || s.variant === DEFAULT_MODELS[s.name]),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aIndex = a.name ? DEFAULT_MODEL_SORTING.indexOf(a.name) : -1
|
||||
const bIndex = b.name ? DEFAULT_MODEL_SORTING.indexOf(b.name) : -1
|
||||
return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex)
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_available_capes(): Promise<Cape[]> {
|
||||
return invoke('plugin:minecraft-skins|get_available_capes', {})
|
||||
}
|
||||
|
||||
export async function get_available_skins(): Promise<Skin[]> {
|
||||
return invoke('plugin:minecraft-skins|get_available_skins', {})
|
||||
}
|
||||
|
||||
export async function add_and_equip_custom_skin(
|
||||
textureBlob: Uint8Array,
|
||||
variant: SkinModel,
|
||||
capeOverride?: Cape,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
|
||||
textureBlob,
|
||||
variant,
|
||||
capeOverride,
|
||||
})
|
||||
}
|
||||
|
||||
export async function set_default_cape(cape?: Cape): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|set_default_cape', {
|
||||
cape,
|
||||
})
|
||||
}
|
||||
|
||||
export async function equip_skin(skin: Skin): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|equip_skin', {
|
||||
skin,
|
||||
})
|
||||
}
|
||||
|
||||
export async function remove_custom_skin(skin: Skin): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|remove_custom_skin', {
|
||||
skin,
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
|
||||
const data = await normalize_skin_texture(skin.texture)
|
||||
const base64 = arrayBufferToBase64(data)
|
||||
return `data:image/png;base64,${base64}`
|
||||
}
|
||||
|
||||
export async function normalize_skin_texture(texture: Uint8Array | string): Promise<Uint8Array> {
|
||||
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { texture })
|
||||
}
|
||||
|
||||
export async function unequip_skin(): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|unequip_skin')
|
||||
}
|
||||
|
||||
export async function get_dragged_skin_data(path: string): Promise<Uint8Array> {
|
||||
const data = await invoke('plugin:minecraft-skins|get_dragged_skin_data', { path })
|
||||
return new Uint8Array(data)
|
||||
}
|
||||
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
interface StoredHead {
|
||||
blob: Blob
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export class HeadStorage {
|
||||
private dbName = 'head-storage'
|
||||
private version = 1
|
||||
private db: IDBDatabase | null = null
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version)
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
resolve()
|
||||
}
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains('heads')) {
|
||||
db.createObjectStore('heads')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async store(key: string, blob: Blob): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
const storedHead: StoredHead = {
|
||||
blob,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.put(storedHead, key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieve(key: string): Promise<string | null> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredHead | undefined
|
||||
|
||||
if (!result) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(result.blob)
|
||||
resolve(url)
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
const results: Record<string, Blob | null> = {}
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
let completedRequests = 0
|
||||
|
||||
if (keys.length === 0) {
|
||||
resolve(results)
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredHead | undefined
|
||||
|
||||
if (result) {
|
||||
results[key] = result.blob
|
||||
} else {
|
||||
results[key] = null
|
||||
}
|
||||
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
results[key] = null
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
let deletedCount = 0
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
|
||||
if (!validKeys.has(key)) {
|
||||
const deleteRequest = cursor.delete()
|
||||
deleteRequest.onsuccess = () => {
|
||||
deletedCount++
|
||||
}
|
||||
deleteRequest.onerror = () => {
|
||||
console.warn('Failed to delete invalid head entry:', key)
|
||||
}
|
||||
}
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
resolve(deletedCount)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async debugCalculateStorage(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
let totalSize = 0
|
||||
let count = 0
|
||||
const entries: Array<{ key: string; size: number }> = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
const value = cursor.value as StoredHead
|
||||
|
||||
const entrySize = value.blob.size
|
||||
totalSize += entrySize
|
||||
count++
|
||||
|
||||
entries.push({
|
||||
key,
|
||||
size: entrySize,
|
||||
})
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
console.group('🗄️ Head Storage Debug Info')
|
||||
console.log(`Total entries: ${count}`)
|
||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||
console.log(
|
||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||
)
|
||||
|
||||
if (entries.length > 0) {
|
||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||
console.log(
|
||||
'Largest entry:',
|
||||
sortedEntries[0].key,
|
||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
console.log(
|
||||
'Smallest entry:',
|
||||
sortedEntries[sortedEntries.length - 1].key,
|
||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async clearAll(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.clear()
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const headStorage = new HeadStorage()
|
||||
218
apps/app-frontend/src/helpers/storage/skin-preview-storage.ts
Normal file
218
apps/app-frontend/src/helpers/storage/skin-preview-storage.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import type { RawRenderResult } from '../rendering/batch-skin-renderer'
|
||||
|
||||
interface StoredPreview {
|
||||
forwards: Blob
|
||||
backwards: Blob
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export class SkinPreviewStorage {
|
||||
private dbName = 'skin-previews'
|
||||
private version = 1
|
||||
private db: IDBDatabase | null = null
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version)
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
resolve()
|
||||
}
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains('previews')) {
|
||||
db.createObjectStore('previews')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async store(key: string, result: RawRenderResult): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
const storedPreview: StoredPreview = {
|
||||
forwards: result.forwards,
|
||||
backwards: result.backwards,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.put(storedPreview, key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieve(key: string): Promise<RawRenderResult | null> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredPreview | undefined
|
||||
|
||||
if (!result) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
resolve({ forwards: result.forwards, backwards: result.backwards })
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
const results: Record<string, RawRenderResult | null> = {}
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
let completedRequests = 0
|
||||
|
||||
if (keys.length === 0) {
|
||||
resolve(results)
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredPreview | undefined
|
||||
|
||||
if (result) {
|
||||
results[key] = { forwards: result.forwards, backwards: result.backwards }
|
||||
} else {
|
||||
results[key] = null
|
||||
}
|
||||
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
results[key] = null
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||
const store = transaction.objectStore('previews')
|
||||
let deletedCount = 0
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
|
||||
if (!validKeys.has(key)) {
|
||||
const deleteRequest = cursor.delete()
|
||||
deleteRequest.onsuccess = () => {
|
||||
deletedCount++
|
||||
}
|
||||
deleteRequest.onerror = () => {
|
||||
console.warn('Failed to delete invalid entry:', key)
|
||||
}
|
||||
}
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
resolve(deletedCount)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async debugCalculateStorage(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
let totalSize = 0
|
||||
let count = 0
|
||||
const entries: Array<{ key: string; size: number }> = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
const value = cursor.value as StoredPreview
|
||||
|
||||
const entrySize = value.forwards.size + value.backwards.size
|
||||
totalSize += entrySize
|
||||
count++
|
||||
|
||||
entries.push({
|
||||
key,
|
||||
size: entrySize,
|
||||
})
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
console.group('🗄️ Skin Preview Storage Debug Info')
|
||||
console.log(`Total entries: ${count}`)
|
||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||
console.log(
|
||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||
)
|
||||
|
||||
if (entries.length > 0) {
|
||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||
console.log(
|
||||
'Largest entry:',
|
||||
sortedEntries[0].key,
|
||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
console.log(
|
||||
'Smallest entry:',
|
||||
sortedEntries[sortedEntries.length - 1].key,
|
||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const skinPreviewStorage = new SkinPreviewStorage()
|
||||
@@ -51,6 +51,7 @@ export type ServerStatus = {
|
||||
version?: {
|
||||
name: string
|
||||
protocol: number
|
||||
legacy: boolean
|
||||
}
|
||||
favicon?: string
|
||||
enforces_secure_chat: boolean
|
||||
@@ -70,11 +71,17 @@ export interface Chat {
|
||||
|
||||
export type ServerData = {
|
||||
refreshing: boolean
|
||||
lastSuccessfulRefresh?: number
|
||||
status?: ServerStatus
|
||||
rawMotd?: string | Chat
|
||||
renderedMotd?: string
|
||||
}
|
||||
|
||||
export type ProtocolVersion = {
|
||||
version: number
|
||||
legacy: boolean
|
||||
}
|
||||
|
||||
export async function get_recent_worlds(
|
||||
limit: number,
|
||||
displayStatuses?: DisplayStatus[],
|
||||
@@ -156,13 +163,13 @@ export async function remove_server_from_profile(path: string, index: number): P
|
||||
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
||||
}
|
||||
|
||||
export async function get_profile_protocol_version(path: string): Promise<number | null> {
|
||||
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
||||
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
||||
}
|
||||
|
||||
export async function get_server_status(
|
||||
address: string,
|
||||
protocolVersion: number | null = null,
|
||||
protocolVersion: ProtocolVersion | null = null,
|
||||
): Promise<ServerStatus> {
|
||||
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
||||
}
|
||||
@@ -206,30 +213,39 @@ export function isServerWorld(world: World): world is ServerWorld {
|
||||
|
||||
export async function refreshServerData(
|
||||
serverData: ServerData,
|
||||
protocolVersion: number | null,
|
||||
protocolVersion: ProtocolVersion | null,
|
||||
address: string,
|
||||
): Promise<void> {
|
||||
const refreshTime = Date.now()
|
||||
serverData.refreshing = true
|
||||
await get_server_status(address, protocolVersion)
|
||||
.then((status) => {
|
||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||
// Don't update if there was a more recent successful refresh
|
||||
return
|
||||
}
|
||||
serverData.lastSuccessfulRefresh = Date.now()
|
||||
serverData.status = status
|
||||
if (status.description) {
|
||||
serverData.rawMotd = status.description
|
||||
serverData.renderedMotd = autoToHTML(status.description)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Refreshing addr: ${address}`, err)
|
||||
})
|
||||
.finally(() => {
|
||||
serverData.refreshing = false
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||
if (!protocolVersion?.legacy) {
|
||||
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function refreshServers(
|
||||
export function refreshServers(
|
||||
worlds: World[],
|
||||
serverData: Record<string, ServerData>,
|
||||
protocolVersion: number | null,
|
||||
protocolVersion: ProtocolVersion | null,
|
||||
) {
|
||||
const servers = worlds.filter(isServerWorld)
|
||||
servers.forEach((server) => {
|
||||
@@ -243,10 +259,8 @@ export async function refreshServers(
|
||||
})
|
||||
|
||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||
Promise.all(
|
||||
Object.keys(serverData).map((address) =>
|
||||
refreshServerData(serverData[address], protocolVersion, address),
|
||||
),
|
||||
Object.keys(serverData).forEach((address) =>
|
||||
refreshServerData(serverData[address], protocolVersion, address),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -377,6 +377,12 @@
|
||||
"instance.worlds.hardcore": {
|
||||
"message": "Hardcore mode"
|
||||
},
|
||||
"instance.worlds.incompatible_server": {
|
||||
"message": "Server is incompatible"
|
||||
},
|
||||
"instance.worlds.no_contact": {
|
||||
"message": "Server couldn't be contacted"
|
||||
},
|
||||
"instance.worlds.no_quick_play": {
|
||||
"message": "You can only jump straight into worlds on Minecraft 1.20+"
|
||||
},
|
||||
|
||||
@@ -220,6 +220,7 @@ async function refreshSearch() {
|
||||
}
|
||||
}
|
||||
results.value = rawResults.result
|
||||
currentPage.value = 1
|
||||
|
||||
const persistentParams: LocationQuery = {}
|
||||
|
||||
@@ -265,6 +266,7 @@ async function onSearchChangeToTop() {
|
||||
|
||||
function clearSearch() {
|
||||
query.value = ''
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -10,6 +10,7 @@ import dayjs from 'dayjs'
|
||||
import { get_search_results } from '@/helpers/cache.js'
|
||||
import type { SearchResult } from '@modrinth/utils'
|
||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
|
||||
const route = useRoute()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
@@ -82,13 +83,15 @@ async function refreshFeaturedProjects() {
|
||||
await fetchInstances()
|
||||
await refreshFeaturedProjects()
|
||||
|
||||
const unlistenProfile = await profile_listener(async (e) => {
|
||||
await fetchInstances()
|
||||
const unlistenProfile = await profile_listener(
|
||||
async (e: { event: string; profile_path_id: string }) => {
|
||||
await fetchInstances()
|
||||
|
||||
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
|
||||
await refreshFeaturedProjects()
|
||||
}
|
||||
})
|
||||
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
|
||||
await refreshFeaturedProjects()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProfile()
|
||||
@@ -97,8 +100,8 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="p-6 flex flex-col gap-2">
|
||||
<h1 v-if="recentInstances" class="m-0 text-2xl">Welcome back!</h1>
|
||||
<h1 v-else class="m-0 text-2xl">Welcome to Modrinth App!</h1>
|
||||
<h1 v-if="recentInstances?.length > 0" class="m-0 text-2xl font-extrabold">Welcome back!</h1>
|
||||
<h1 v-else class="m-0 text-2xl font-extrabold">Welcome to Modrinth App!</h1>
|
||||
<RecentWorldsList :recent-instances="recentInstances" />
|
||||
<RowDisplay
|
||||
v-if="hasFeaturedProjects"
|
||||
|
||||
521
apps/app-frontend/src/pages/Skins.vue
Normal file
521
apps/app-frontend/src/pages/Skins.vue
Normal file
@@ -0,0 +1,521 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EditIcon,
|
||||
ExcitedRinthbot,
|
||||
LogInIcon,
|
||||
PlusIcon,
|
||||
SpinnerIcon,
|
||||
TrashIcon,
|
||||
UpdatedIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
ConfirmModal,
|
||||
SkinButton,
|
||||
SkinLikeTextButton,
|
||||
SkinPreviewRenderer,
|
||||
} from '@modrinth/ui'
|
||||
import { computedAsync } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
|
||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||
import { handleError, useNotifications } from '@/store/notifications'
|
||||
import type { Cape, Skin } from '@/helpers/skins.ts'
|
||||
import {
|
||||
normalize_skin_texture,
|
||||
equip_skin,
|
||||
filterDefaultSkins,
|
||||
filterSavedSkins,
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
get_normalized_skin_texture,
|
||||
remove_custom_skin,
|
||||
set_default_cape,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
const editSkinModal = useTemplateRef('editSkinModal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
const uploadSkinModal = useTemplateRef('uploadSkinModal')
|
||||
|
||||
const notifications = useNotifications()
|
||||
|
||||
const settings = ref(await getSettings())
|
||||
const skins = ref<Skin[]>([])
|
||||
const capes = ref<Cape[]>([])
|
||||
|
||||
const accountsCard = inject('accountsCard') as Ref<typeof AccountsCard>
|
||||
const currentUser = ref(undefined)
|
||||
const currentUserId = ref<string | undefined>(undefined)
|
||||
|
||||
const username = computed(() => currentUser.value?.profile?.name ?? undefined)
|
||||
const selectedSkin = ref<Skin | null>(null)
|
||||
const defaultCape = ref<Cape>()
|
||||
|
||||
const originalSelectedSkin = ref<Skin | null>(null)
|
||||
const originalDefaultCape = ref<Cape>()
|
||||
|
||||
const savedSkins = computed(() => filterSavedSkins(skins.value))
|
||||
const defaultSkins = computed(() => filterDefaultSkins(skins.value))
|
||||
|
||||
const currentCape = computed(() => {
|
||||
if (selectedSkin.value?.cape_id) {
|
||||
const overrideCape = capes.value.find((c) => c.id === selectedSkin.value?.cape_id)
|
||||
if (overrideCape) {
|
||||
return overrideCape
|
||||
}
|
||||
}
|
||||
return defaultCape.value
|
||||
})
|
||||
|
||||
const skinTexture = computedAsync(async () => {
|
||||
if (selectedSkin.value?.texture) {
|
||||
return await get_normalized_skin_texture(selectedSkin.value)
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const capeTexture = computed(() => currentCape.value?.texture)
|
||||
const skinVariant = computed(() => selectedSkin.value?.variant)
|
||||
const skinNametag = computed(() =>
|
||||
settings.value.hide_nametag_skins_page ? undefined : username.value,
|
||||
)
|
||||
|
||||
let userCheckInterval: number | null = null
|
||||
|
||||
const deleteSkinModal = ref()
|
||||
const skinToDelete = ref<Skin | null>(null)
|
||||
|
||||
function confirmDeleteSkin(skin: Skin) {
|
||||
skinToDelete.value = skin
|
||||
deleteSkinModal.value?.show()
|
||||
}
|
||||
|
||||
async function deleteSkin() {
|
||||
if (!skinToDelete.value) return
|
||||
await remove_custom_skin(skinToDelete.value).catch(handleError)
|
||||
await loadSkins()
|
||||
skinToDelete.value = null
|
||||
}
|
||||
|
||||
async function loadCapes() {
|
||||
try {
|
||||
capes.value = (await get_available_capes()) ?? []
|
||||
defaultCape.value = capes.value.find((c) => c.is_equipped)
|
||||
originalDefaultCape.value = defaultCape.value
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkins() {
|
||||
try {
|
||||
skins.value = (await get_available_skins()) ?? []
|
||||
generateSkinPreviews(skins.value, capes.value)
|
||||
selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
|
||||
originalSelectedSkin.value = selectedSkin.value
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function changeSkin(newSkin: Skin) {
|
||||
const previousSkin = selectedSkin.value
|
||||
const previousSkinsList = [...skins.value]
|
||||
|
||||
skins.value = skins.value.map((skin) => {
|
||||
return {
|
||||
...skin,
|
||||
is_equipped: skin.texture_key === newSkin.texture_key,
|
||||
}
|
||||
})
|
||||
|
||||
selectedSkin.value = skins.value.find((s) => s.texture_key === newSkin.texture_key) || null
|
||||
|
||||
try {
|
||||
await equip_skin(newSkin)
|
||||
if (accountsCard.value) {
|
||||
await accountsCard.value.refreshValues()
|
||||
}
|
||||
} catch (error) {
|
||||
selectedSkin.value = previousSkin
|
||||
skins.value = previousSkinsList
|
||||
|
||||
if ((error as { message?: string })?.message?.includes('429 Too Many Requests')) {
|
||||
notifications.addNotification({
|
||||
type: 'error',
|
||||
title: 'Slow down!',
|
||||
text: "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
||||
})
|
||||
} else {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCapeSelected(cape: Cape | undefined) {
|
||||
const previousDefaultCape = defaultCape.value
|
||||
const previousCapesList = [...capes.value]
|
||||
|
||||
capes.value = capes.value.map((c) => ({
|
||||
...c,
|
||||
is_equipped: cape ? c.id === cape.id : false,
|
||||
}))
|
||||
|
||||
defaultCape.value = cape ? capes.value.find((c) => c.id === cape.id) : undefined
|
||||
|
||||
try {
|
||||
await set_default_cape(cape)
|
||||
} catch (error) {
|
||||
defaultCape.value = previousDefaultCape
|
||||
capes.value = previousCapesList
|
||||
|
||||
if ((error as { message?: string })?.message?.includes('429 Too Many Requests')) {
|
||||
notifications.addNotification({
|
||||
type: 'error',
|
||||
title: 'Slow down!',
|
||||
text: "You're changing your cape too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
||||
})
|
||||
} else {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onSkinSaved() {
|
||||
await Promise.all([loadCapes(), loadSkins()])
|
||||
}
|
||||
|
||||
async function loadCurrentUser() {
|
||||
try {
|
||||
const defaultId = await get_default_user()
|
||||
currentUserId.value = defaultId
|
||||
|
||||
const allAccounts = await users()
|
||||
currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId)
|
||||
} catch (e) {
|
||||
handleError(e)
|
||||
currentUser.value = undefined
|
||||
currentUserId.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
return skinBlobUrlMap.get(key)
|
||||
}
|
||||
|
||||
async function login() {
|
||||
accountsCard.value.setLoginDisabled(true)
|
||||
const loggedIn = await login_flow().catch(handleSevereError)
|
||||
|
||||
if (loggedIn && accountsCard) {
|
||||
await accountsCard.value.refreshValues()
|
||||
}
|
||||
|
||||
trackEvent('AccountLogIn')
|
||||
accountsCard.value.setLoginDisabled(false)
|
||||
}
|
||||
|
||||
function openUploadSkinModal(e: MouseEvent) {
|
||||
uploadSkinModal.value?.show(e)
|
||||
}
|
||||
|
||||
function onSkinFileUploaded(buffer: ArrayBuffer) {
|
||||
const fakeEvent = new MouseEvent('click')
|
||||
normalize_skin_texture(`data:image/png;base64,` + arrayBufferToBase64(buffer)).then(
|
||||
(skinTextureNormalized: Uint8Array) => {
|
||||
const skinTexUrl = `data:image/png;base64,` + arrayBufferToBase64(skinTextureNormalized)
|
||||
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
|
||||
editSkinModal.value.restoreWithNewTexture(skinTexUrl)
|
||||
} else {
|
||||
editSkinModal.value?.showNew(fakeEvent, skinTexUrl)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function onUploadCanceled() {
|
||||
editSkinModal.value?.restoreModal()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => selectedSkin.value?.cape_id,
|
||||
() => {},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
userCheckInterval = window.setInterval(checkUserChanges, 250)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (userCheckInterval !== null) {
|
||||
window.clearInterval(userCheckInterval)
|
||||
}
|
||||
})
|
||||
|
||||
async function checkUserChanges() {
|
||||
try {
|
||||
const defaultId = await get_default_user()
|
||||
if (defaultId !== currentUserId.value) {
|
||||
await loadCurrentUser()
|
||||
await loadCapes()
|
||||
await loadSkins()
|
||||
}
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EditSkinModal
|
||||
ref="editSkinModal"
|
||||
:capes="capes"
|
||||
:default-cape="defaultCape"
|
||||
@saved="onSkinSaved"
|
||||
@deleted="() => loadSkins()"
|
||||
@open-upload-modal="openUploadSkinModal"
|
||||
/>
|
||||
<SelectCapeModal ref="selectCapeModal" :capes="capes" @select="handleCapeSelected" />
|
||||
<UploadSkinModal
|
||||
ref="uploadSkinModal"
|
||||
@uploaded="onSkinFileUploaded"
|
||||
@canceled="onUploadCanceled"
|
||||
/>
|
||||
<ConfirmModal
|
||||
ref="deleteSkinModal"
|
||||
title="Are you sure you want to delete this skin?"
|
||||
description="This will permanently delete the selected skin. This action cannot be undone."
|
||||
proceed-label="Delete"
|
||||
@proceed="deleteSkin"
|
||||
/>
|
||||
|
||||
<div v-if="currentUser" class="p-4 skin-layout">
|
||||
<div class="preview-panel">
|
||||
<h1 class="m-0 text-2xl font-bold flex items-center gap-2">
|
||||
Skins
|
||||
<span class="text-sm font-bold px-2 bg-brand-highlight text-brand rounded-full">Beta</span>
|
||||
</h1>
|
||||
<div class="preview-container">
|
||||
<SkinPreviewRenderer
|
||||
:cape-src="capeTexture"
|
||||
:texture-src="skinTexture || ''"
|
||||
:variant="skinVariant"
|
||||
:nametag="skinNametag"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
>
|
||||
<template #subtitle>
|
||||
<ButtonStyled :disabled="!!selectedSkin?.cape_id">
|
||||
<button
|
||||
v-tooltip="
|
||||
selectedSkin?.cape_id
|
||||
? 'The equipped skin is overriding the default cape.'
|
||||
: undefined
|
||||
"
|
||||
:disabled="!!selectedSkin?.cape_id"
|
||||
@click="
|
||||
(e: MouseEvent) =>
|
||||
selectCapeModal?.show(
|
||||
e,
|
||||
selectedSkin?.texture_key,
|
||||
currentCape,
|
||||
skinTexture,
|
||||
skinVariant,
|
||||
)
|
||||
"
|
||||
>
|
||||
<UpdatedIcon />
|
||||
Change cape
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</SkinPreviewRenderer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="skins-container">
|
||||
<section class="flex flex-col gap-2 mt-1">
|
||||
<h2 class="text-lg font-bold m-0 text-primary">Saved skins</h2>
|
||||
<div class="skin-card-grid">
|
||||
<SkinLikeTextButton class="skin-card" @click="openUploadSkinModal">
|
||||
<template #icon>
|
||||
<PlusIcon class="size-8" />
|
||||
</template>
|
||||
<span>Add a skin</span>
|
||||
</SkinLikeTextButton>
|
||||
|
||||
<SkinButton
|
||||
v-for="skin in savedSkins"
|
||||
:key="`saved-skin-${skin.texture_key}`"
|
||||
class="skin-card"
|
||||
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
|
||||
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
|
||||
:selected="selectedSkin === skin"
|
||||
@select="changeSkin(skin)"
|
||||
>
|
||||
<template #overlay-buttons>
|
||||
<Button
|
||||
color="green"
|
||||
aria-label="Edit skin"
|
||||
class="pointer-events-auto"
|
||||
@click.stop="(e) => editSkinModal?.show(e, skin)"
|
||||
>
|
||||
<EditIcon /> Edit
|
||||
</Button>
|
||||
<Button
|
||||
v-show="!skin.is_equipped"
|
||||
v-tooltip="'Delete skin'"
|
||||
aria-label="Delete skin"
|
||||
color="red"
|
||||
class="!rounded-[100%] pointer-events-auto"
|
||||
icon-only
|
||||
@click.stop="() => confirmDeleteSkin(skin)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</template>
|
||||
</SkinButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col gap-2 mt-6">
|
||||
<h2 class="text-lg font-bold m-0 text-primary">Default skins</h2>
|
||||
<div class="skin-card-grid">
|
||||
<SkinButton
|
||||
v-for="skin in defaultSkins"
|
||||
:key="`default-skin-${skin.texture_key}`"
|
||||
class="skin-card"
|
||||
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
|
||||
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
|
||||
:selected="selectedSkin === skin"
|
||||
:tooltip="skin.name"
|
||||
@select="changeSkin(skin)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex items-center justify-center min-h-[50vh] pt-[25%]">
|
||||
<div
|
||||
class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
|
||||
>
|
||||
<img
|
||||
:src="ExcitedRinthbot"
|
||||
alt="Excited Modrinth Bot"
|
||||
class="absolute -top-28 right-8 md:right-20 h-28 w-auto"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-[1px] opacity-40 bg-gradient-to-r from-transparent via-green-500 to-transparent"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 2rem,
|
||||
var(--color-green) calc(100% - 13rem),
|
||||
var(--color-green) calc(100% - 5rem),
|
||||
transparent calc(100% - 2rem)
|
||||
);
|
||||
"
|
||||
></div>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
<h1 class="text-3xl font-extrabold m-0">Please sign-in</h1>
|
||||
<p class="text-lg m-0">
|
||||
Please sign into your Minecraft account to use the skin management features of the
|
||||
Modrinth app.
|
||||
</p>
|
||||
<ButtonStyled v-show="accountsCard" color="brand" :disabled="accountsCard.loginDisabled">
|
||||
<button :disabled="accountsCard.loginDisabled" @click="login">
|
||||
<LogInIcon v-if="!accountsCard.loginDisabled" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
Sign In
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$skin-card-width: 155px;
|
||||
$skin-card-gap: 4px;
|
||||
|
||||
.skin-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 2.5fr);
|
||||
gap: 2.5rem;
|
||||
|
||||
@media (max-width: 700px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
top: 1.5rem;
|
||||
position: sticky;
|
||||
align-self: start;
|
||||
padding: 0.5rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: calc((2.5rem / 2));
|
||||
|
||||
@media (max-width: 700px) {
|
||||
height: 50vh;
|
||||
}
|
||||
}
|
||||
|
||||
.skins-container {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.skin-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $skin-card-gap;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 1300px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 1750px) {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 2050px) {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.skin-card {
|
||||
aspect-ratio: 0.95;
|
||||
border-radius: 10px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
import Index from './Index.vue'
|
||||
import Browse from './Browse.vue'
|
||||
import Worlds from './Worlds.vue'
|
||||
import Skins from './Skins.vue'
|
||||
|
||||
export { Index, Browse, Worlds }
|
||||
export { Index, Browse, Worlds, Skins }
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="instance.install_stage.includes('installing')"
|
||||
v-if="
|
||||
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
||||
instance.install_stage,
|
||||
)
|
||||
"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
|
||||
@@ -483,7 +483,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: calc(100vh - 11rem);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
||||
"
|
||||
@delete="() => promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,6 +134,7 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { PlusIcon, SpinnerIcon, UpdatedIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
type ProtocolVersion,
|
||||
type SingleplayerWorld,
|
||||
type World,
|
||||
type ServerWorld,
|
||||
@@ -151,6 +153,7 @@ import {
|
||||
hasQuickPlaySupport,
|
||||
refreshWorlds,
|
||||
handleDefaultProfileUpdateEvent,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||
@@ -208,7 +211,9 @@ const worldPlaying = ref<World>()
|
||||
const worlds = ref<World[]>([])
|
||||
const serverData = ref<Record<string, ServerData>>({})
|
||||
|
||||
const protocolVersion = ref<number | null>(await get_profile_protocol_version(instance.value.path))
|
||||
const protocolVersion = ref<ProtocolVersion | null>(
|
||||
await get_profile_protocol_version(instance.value.path),
|
||||
)
|
||||
|
||||
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||
if (e.profile_path_id !== instance.value.path) return
|
||||
@@ -244,7 +249,7 @@ async function refreshAllWorlds() {
|
||||
worlds.value = await refreshWorlds(instance.value.path).finally(
|
||||
() => (refreshingAll.value = false),
|
||||
)
|
||||
await refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||
|
||||
const hasNoWorlds = worlds.value.length === 0
|
||||
|
||||
|
||||
@@ -34,6 +34,14 @@ export default new createRouter({
|
||||
breadcrumb: [{ name: 'Discover content' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/skins',
|
||||
name: 'Skins',
|
||||
component: Pages.Skins,
|
||||
meta: {
|
||||
breadcrumb: [{ name: 'Skins' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/library',
|
||||
name: 'Library',
|
||||
|
||||
@@ -41,6 +41,7 @@ export default {
|
||||
green: 'var(--color-green-highlight)',
|
||||
blue: 'var(--color-blue-highlight)',
|
||||
purple: 'var(--color-purple-highlight)',
|
||||
gray: 'var(--color-gray-highlight)',
|
||||
},
|
||||
divider: {
|
||||
DEFAULT: 'var(--color-divider)',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
"strict": true
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ import svgLoader from 'vite-svg-loader'
|
||||
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
import tauriConf from '../app/tauri.conf.json'
|
||||
|
||||
const projectRootDir = resolve(__dirname)
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@@ -41,17 +43,32 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
headers: {
|
||||
'content-security-policy': Object.entries(tauriConf.app.security.csp)
|
||||
.map(([directive, sources]) => {
|
||||
// An additional websocket connect-src is required for Vite dev tools to work
|
||||
if (directive === 'connect-src') {
|
||||
sources = Array.isArray(sources) ? sources : [sources]
|
||||
sources.push('ws://localhost:1420')
|
||||
}
|
||||
|
||||
return Array.isArray(sources)
|
||||
? `${directive} ${sources.join(' ')}`
|
||||
: `${directive} ${sources}`
|
||||
})
|
||||
.join('; '),
|
||||
},
|
||||
},
|
||||
// to make use of `TAURI_ENV_DEBUG` and other env variables
|
||||
// https://v2.tauri.app/reference/environment-variables/#tauri-cli-hook-commands
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
build: {
|
||||
// Tauri supports es2021
|
||||
target: process.env.TAURI_PLATFORM == 'windows' ? 'chrome105' : 'safari13', // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
target: process.env.TAURI_ENV_PLATFORM == 'windows' ? 'chrome105' : 'safari13', // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
// don't minify for debug builds
|
||||
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
// produce sourcemaps for debug builds
|
||||
sourcemap: !!process.env.TAURI_DEBUG, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
sourcemap: !!process.env.TAURI_ENV_DEBUG, // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
commonjsOptions: {
|
||||
esmExternals: true,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "theseus_playground"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -9,3 +9,6 @@ edition = "2024"
|
||||
theseus = { workspace = true, features = ["cli"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
enumset.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"name": "@modrinth/app-playground",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||
println!("A browser window will now open, follow the login flow there.");
|
||||
let login = minecraft_auth::begin_login().await?;
|
||||
|
||||
println!("Open URL {} in a browser", login.redirect_uri.as_str());
|
||||
println!("Open URL {} in a browser", login.auth_request_uri.as_str());
|
||||
|
||||
println!("Please enter URL code: ");
|
||||
let mut input = String::new();
|
||||
@@ -27,7 +27,10 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||
|
||||
let credentials = minecraft_auth::finish_login(&input, login).await?;
|
||||
|
||||
println!("Logged in user {}.", credentials.username);
|
||||
println!(
|
||||
"Logged in user {}.",
|
||||
credentials.maybe_online_profile().await.name
|
||||
);
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
|
||||
4
apps/app/.gitignore
vendored
4
apps/app/.gitignore
vendored
@@ -1,6 +1,2 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by tauri, metadata generated at compile time
|
||||
/gen/
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.9.5"
|
||||
version = "1.0.0-local" # The actual version is set by the theseus-build workflow on tagging
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
edition.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { workspace = true, features = ["codegen"] }
|
||||
@@ -17,20 +16,23 @@ serde_json.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_with.workspace = true
|
||||
|
||||
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||
tauri-plugin-window-state.workspace = true
|
||||
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset"] }
|
||||
tauri-plugin-deep-link.workspace = true
|
||||
tauri-plugin-os.workspace = true
|
||||
tauri-plugin-opener.workspace = true
|
||||
tauri-plugin-dialog.workspace = true
|
||||
tauri-plugin-updater.workspace = true
|
||||
tauri-plugin-http.workspace = true
|
||||
tauri-plugin-opener.workspace = true
|
||||
tauri-plugin-os.workspace = true
|
||||
tauri-plugin-single-instance.workspace = true
|
||||
tauri-plugin-updater.workspace = true
|
||||
tauri-plugin-window-state.workspace = true
|
||||
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
thiserror.workspace = true
|
||||
daedalus.workspace = true
|
||||
chrono.workspace = true
|
||||
either.workspace = true
|
||||
hyper = { workspace = true, features = ["server"] }
|
||||
hyper-util.workspace = true
|
||||
|
||||
url.workspace = true
|
||||
urlencoding.workspace = true
|
||||
@@ -56,3 +58,6 @@ default = ["custom-protocol"]
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
updater = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -18,5 +18,25 @@
|
||||
<string>A Minecraft mod wants to access your camera.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>A Minecraft mod wants to access your microphone.</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>asset.localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>textures.minecraft.net</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -99,10 +99,33 @@ fn main() {
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"minecraft-skins",
|
||||
InlinedPlugin::new()
|
||||
.commands(&[
|
||||
"get_available_capes",
|
||||
"get_available_skins",
|
||||
"add_and_equip_custom_skin",
|
||||
"set_default_cape",
|
||||
"equip_skin",
|
||||
"remove_custom_skin",
|
||||
"unequip_skin",
|
||||
"normalize_skin_texture",
|
||||
"get_dragged_skin_data",
|
||||
])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"mr-auth",
|
||||
InlinedPlugin::new()
|
||||
.commands(&["modrinth_login", "logout", "get"])
|
||||
.commands(&[
|
||||
"modrinth_login",
|
||||
"logout",
|
||||
"get",
|
||||
"cancel_modrinth_login",
|
||||
])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
@@ -151,7 +174,6 @@ fn main() {
|
||||
"profile_update_managed_modrinth_version",
|
||||
"profile_repair_managed_modrinth",
|
||||
"profile_run",
|
||||
"profile_run_credentials",
|
||||
"profile_kill",
|
||||
"profile_edit",
|
||||
"profile_edit_icon",
|
||||
|
||||
@@ -19,12 +19,21 @@
|
||||
"window-state:default",
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state",
|
||||
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "https://modrinth.com/*" },
|
||||
{ "url": "https://*.modrinth.com/*" }
|
||||
]
|
||||
},
|
||||
|
||||
"auth:default",
|
||||
"import:default",
|
||||
"jre:default",
|
||||
"logs:default",
|
||||
"metadata:default",
|
||||
"minecraft-skins:default",
|
||||
"mr-auth:default",
|
||||
"profile-create:default",
|
||||
"pack:default",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@modrinth/app",
|
||||
"scripts": {
|
||||
"build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev",
|
||||
"test": "cargo test",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.5.0"
|
||||
|
||||
@@ -33,7 +33,7 @@ pub async fn login<R: Runtime>(
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"signin",
|
||||
tauri::WebviewUrl::External(flow.redirect_uri.parse().map_err(
|
||||
tauri::WebviewUrl::External(flow.auth_request_uri.parse().map_err(
|
||||
|_| {
|
||||
theseus::ErrorKind::OtherError(
|
||||
"Error parsing auth redirect URL".to_string(),
|
||||
@@ -77,6 +77,7 @@ pub async fn login<R: Runtime>(
|
||||
window.close()?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
|
||||
Ok(minecraft_auth::remove_user(user).await?)
|
||||
|
||||
@@ -41,8 +41,8 @@ pub async fn jre_find_filtered_jres(
|
||||
// Validates JRE at a given path
|
||||
// Returns None if the path is not a valid JRE
|
||||
#[tauri::command]
|
||||
pub async fn jre_get_jre(path: PathBuf) -> Result<Option<JavaVersion>> {
|
||||
jre::check_jre(path).await.map_err(|e| e.into())
|
||||
pub async fn jre_get_jre(path: PathBuf) -> Result<JavaVersion> {
|
||||
Ok(jre::check_jre(path).await?)
|
||||
}
|
||||
|
||||
// Tests JRE of a certain version
|
||||
|
||||
104
apps/app/src/api/minecraft_skins.rs
Normal file
104
apps/app/src/api/minecraft_skins.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use crate::api::Result;
|
||||
|
||||
use std::path::Path;
|
||||
use theseus::minecraft_skins::{
|
||||
self, Bytes, Cape, MinecraftSkinVariant, Skin, UrlOrBlob,
|
||||
};
|
||||
|
||||
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("minecraft-skins")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
add_and_equip_custom_skin,
|
||||
set_default_cape,
|
||||
equip_skin,
|
||||
remove_custom_skin,
|
||||
unequip_skin,
|
||||
normalize_skin_texture,
|
||||
get_dragged_skin_data,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_available_capes')`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_available_capes]
|
||||
#[tauri::command]
|
||||
pub async fn get_available_capes() -> Result<Vec<Cape>> {
|
||||
Ok(minecraft_skins::get_available_capes().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_available_skins')`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_available_skins]
|
||||
#[tauri::command]
|
||||
pub async fn get_available_skins() -> Result<Vec<Skin>> {
|
||||
Ok(minecraft_skins::get_available_skins().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape_override)`
|
||||
///
|
||||
/// See also: [minecraft_skins::add_and_equip_custom_skin]
|
||||
#[tauri::command]
|
||||
pub async fn add_and_equip_custom_skin(
|
||||
texture_blob: Bytes,
|
||||
variant: MinecraftSkinVariant,
|
||||
cape_override: Option<Cape>,
|
||||
) -> Result<()> {
|
||||
Ok(minecraft_skins::add_and_equip_custom_skin(
|
||||
texture_blob,
|
||||
variant,
|
||||
cape_override,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|set_default_cape', cape)`
|
||||
///
|
||||
/// See also: [minecraft_skins::set_default_cape]
|
||||
#[tauri::command]
|
||||
pub async fn set_default_cape(cape: Option<Cape>) -> Result<()> {
|
||||
Ok(minecraft_skins::set_default_cape(cape).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|equip_skin', skin)`
|
||||
///
|
||||
/// See also: [minecraft_skins::equip_skin]
|
||||
#[tauri::command]
|
||||
pub async fn equip_skin(skin: Skin) -> Result<()> {
|
||||
Ok(minecraft_skins::equip_skin(skin).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|remove_custom_skin', skin)`
|
||||
///
|
||||
/// See also: [minecraft_skins::remove_custom_skin]
|
||||
#[tauri::command]
|
||||
pub async fn remove_custom_skin(skin: Skin) -> Result<()> {
|
||||
Ok(minecraft_skins::remove_custom_skin(skin).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|unequip_skin')`
|
||||
///
|
||||
/// See also: [minecraft_skins::unequip_skin]
|
||||
#[tauri::command]
|
||||
pub async fn unequip_skin() -> Result<()> {
|
||||
Ok(minecraft_skins::unequip_skin().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|normalize_skin_texture')`
|
||||
///
|
||||
/// See also: [minecraft_skins::normalize_skin_texture]
|
||||
#[tauri::command]
|
||||
pub async fn normalize_skin_texture(texture: UrlOrBlob) -> Result<Bytes> {
|
||||
Ok(minecraft_skins::normalize_skin_texture(&texture).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_dragged_skin_data', path)`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_dragged_skin_data]
|
||||
#[tauri::command]
|
||||
pub async fn get_dragged_skin_data(path: String) -> Result<Bytes> {
|
||||
let path = Path::new(&path);
|
||||
Ok(minecraft_skins::get_dragged_skin_data(path).await?)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod import;
|
||||
pub mod jre;
|
||||
pub mod logs;
|
||||
pub mod metadata;
|
||||
pub mod minecraft_skins;
|
||||
pub mod mr_auth;
|
||||
pub mod pack;
|
||||
pub mod process;
|
||||
@@ -21,6 +22,8 @@ pub mod cache;
|
||||
pub mod friends;
|
||||
pub mod worlds;
|
||||
|
||||
mod oauth_utils;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
||||
|
||||
// // Main returnable Theseus GUI error
|
||||
|
||||
@@ -1,79 +1,70 @@
|
||||
use crate::api::Result;
|
||||
use chrono::{Duration, Utc};
|
||||
use crate::api::TheseusSerializableError;
|
||||
use crate::api::oauth_utils;
|
||||
use tauri::Manager;
|
||||
use tauri::Runtime;
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{Manager, Runtime, UserAttentionType};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use theseus::prelude::*;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("mr-auth")
|
||||
.invoke_handler(tauri::generate_handler![modrinth_login, logout, get,])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
modrinth_login,
|
||||
logout,
|
||||
get,
|
||||
cancel_modrinth_login,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn modrinth_login<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
) -> Result<Option<ModrinthCredentials>> {
|
||||
let redirect_uri = mr_auth::authenticate_begin_flow();
|
||||
) -> Result<ModrinthCredentials> {
|
||||
let (auth_code_recv_socket_tx, auth_code_recv_socket) = oneshot::channel();
|
||||
let auth_code = tokio::spawn(oauth_utils::auth_code_reply::listen(
|
||||
auth_code_recv_socket_tx,
|
||||
));
|
||||
|
||||
let start = Utc::now();
|
||||
let auth_code_recv_socket = auth_code_recv_socket.await.unwrap()?;
|
||||
|
||||
if let Some(window) = app.get_webview_window("modrinth-signin") {
|
||||
window.close()?;
|
||||
}
|
||||
let auth_request_uri = format!(
|
||||
"{}?launcher=true&ipver={}&port={}",
|
||||
mr_auth::authenticate_begin_flow(),
|
||||
if auth_code_recv_socket.is_ipv4() {
|
||||
"4"
|
||||
} else {
|
||||
"6"
|
||||
},
|
||||
auth_code_recv_socket.port()
|
||||
);
|
||||
|
||||
let window = tauri::WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"modrinth-signin",
|
||||
tauri::WebviewUrl::External(redirect_uri.parse().map_err(|_| {
|
||||
theseus::ErrorKind::OtherError(
|
||||
"Error parsing auth redirect URL".to_string(),
|
||||
app.opener()
|
||||
.open_url(auth_request_uri, None::<&str>)
|
||||
.map_err(|e| {
|
||||
TheseusSerializableError::Theseus(
|
||||
theseus::ErrorKind::OtherError(format!(
|
||||
"Failed to open auth request URI: {e}"
|
||||
))
|
||||
.into(),
|
||||
)
|
||||
.as_error()
|
||||
})?),
|
||||
)
|
||||
.min_inner_size(420.0, 632.0)
|
||||
.inner_size(420.0, 632.0)
|
||||
.max_inner_size(420.0, 632.0)
|
||||
.zoom_hotkeys_enabled(false)
|
||||
.title("Sign into Modrinth")
|
||||
.always_on_top(true)
|
||||
.center()
|
||||
.build()?;
|
||||
})?;
|
||||
|
||||
window.request_user_attention(Some(UserAttentionType::Critical))?;
|
||||
let Some(auth_code) = auth_code.await.unwrap()? else {
|
||||
return Err(TheseusSerializableError::Theseus(
|
||||
theseus::ErrorKind::OtherError("Login canceled".into()).into(),
|
||||
));
|
||||
};
|
||||
|
||||
while (Utc::now() - start) < Duration::minutes(10) {
|
||||
if window.title().is_err() {
|
||||
// user closed window, cancelling flow
|
||||
return Ok(None);
|
||||
}
|
||||
let credentials = mr_auth::authenticate_finish_flow(&auth_code).await?;
|
||||
|
||||
if window
|
||||
.url()?
|
||||
.as_str()
|
||||
.starts_with("https://launcher-files.modrinth.com")
|
||||
{
|
||||
let url = window.url()?;
|
||||
|
||||
let code = url.query_pairs().find(|(key, _)| key == "code");
|
||||
|
||||
window.close()?;
|
||||
|
||||
return if let Some((_, code)) = code {
|
||||
let val = mr_auth::authenticate_finish_flow(&code).await?;
|
||||
|
||||
Ok(Some(val))
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
if let Some(main_window) = app.get_window("main") {
|
||||
main_window.set_focus().ok();
|
||||
}
|
||||
|
||||
window.close()?;
|
||||
Ok(None)
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -85,3 +76,8 @@ pub async fn logout() -> Result<()> {
|
||||
pub async fn get() -> Result<Option<ModrinthCredentials>> {
|
||||
Ok(theseus::mr_auth::get_credentials().await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_modrinth_login() {
|
||||
oauth_utils::auth_code_reply::stop_listeners();
|
||||
}
|
||||
|
||||
159
apps/app/src/api/oauth_utils/auth_code_reply.rs
Normal file
159
apps/app/src/api/oauth_utils/auth_code_reply.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
//! A minimal OAuth 2.0 authorization code grant flow redirection/reply loopback URI HTTP
|
||||
//! server implementation, compliant with [RFC 6749]'s authorization code grant flow and
|
||||
//! [RFC 8252]'s best current practices for OAuth 2.0 in native apps.
|
||||
//!
|
||||
//! This server is needed for the step 4 of the OAuth authentication dance represented in
|
||||
//! figure 1 of [RFC 8252].
|
||||
//!
|
||||
//! Further reading: https://www.oauth.com/oauth2-servers/oauth-native-apps/redirect-urls-for-native-apps/
|
||||
//!
|
||||
//! [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
||||
//! [RFC 8252]: https://datatracker.ietf.org/doc/html/rfc8252
|
||||
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
sync::{LazyLock, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use hyper::body::Incoming;
|
||||
use hyper_util::rt::{TokioIo, TokioTimer};
|
||||
use theseus::ErrorKind;
|
||||
use tokio::{
|
||||
net::TcpListener,
|
||||
sync::{broadcast, oneshot},
|
||||
};
|
||||
|
||||
static SERVER_SHUTDOWN: LazyLock<broadcast::Sender<()>> =
|
||||
LazyLock::new(|| broadcast::channel(1024).0);
|
||||
|
||||
/// Starts a temporary HTTP server to receive OAuth 2.0 authorization code grant flow redirects
|
||||
/// on a loopback interface with an ephemeral port. The caller can know the bound socket address
|
||||
/// by listening on the counterpart channel for `listen_socket_tx`.
|
||||
///
|
||||
/// If the server is stopped before receiving an authorization code, `Ok(None)` is returned.
|
||||
pub async fn listen(
|
||||
listen_socket_tx: oneshot::Sender<Result<SocketAddr, theseus::Error>>,
|
||||
) -> Result<Option<String>, theseus::Error> {
|
||||
// IPv4 is tried first for the best compatibility and performance with most systems.
|
||||
// IPv6 is also tried in case IPv4 is not available. Resolving "localhost" is avoided
|
||||
// to prevent failures deriving from improper name resolution setup. Any available
|
||||
// ephemeral port is used to prevent conflicts with other services. This is all as per
|
||||
// RFC 8252's recommendations
|
||||
const ANY_LOOPBACK_SOCKET: &[SocketAddr] = &[
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
||||
];
|
||||
|
||||
let listener = match TcpListener::bind(ANY_LOOPBACK_SOCKET).await {
|
||||
Ok(listener) => {
|
||||
listen_socket_tx
|
||||
.send(listener.local_addr().map_err(|e| {
|
||||
ErrorKind::OtherError(format!(
|
||||
"Failed to get auth code reply socket address: {e}"
|
||||
))
|
||||
.into()
|
||||
}))
|
||||
.ok();
|
||||
|
||||
listener
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg =
|
||||
format!("Failed to bind auth code reply socket: {e}");
|
||||
|
||||
listen_socket_tx
|
||||
.send(Err(ErrorKind::OtherError(error_msg.clone()).into()))
|
||||
.ok();
|
||||
|
||||
return Err(ErrorKind::OtherError(error_msg).into());
|
||||
}
|
||||
};
|
||||
|
||||
let mut auth_code = Mutex::new(None);
|
||||
let mut shutdown_notification = SERVER_SHUTDOWN.subscribe();
|
||||
|
||||
while auth_code.get_mut().unwrap().is_none() {
|
||||
let client_socket = tokio::select! {
|
||||
biased;
|
||||
_ = shutdown_notification.recv() => {
|
||||
break;
|
||||
}
|
||||
conn_accept_result = listener.accept() => {
|
||||
match conn_accept_result {
|
||||
Ok((socket, _)) => socket,
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to accept auth code reply: {e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = hyper::server::conn::http1::Builder::new()
|
||||
.keep_alive(false)
|
||||
.header_read_timeout(Duration::from_secs(5))
|
||||
.timer(TokioTimer::new())
|
||||
.auto_date_header(false)
|
||||
.serve_connection(
|
||||
TokioIo::new(client_socket),
|
||||
hyper::service::service_fn(|req| handle_reply(req, &auth_code)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("Failed to handle auth code reply: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(auth_code.into_inner().unwrap())
|
||||
}
|
||||
|
||||
/// Stops any active OAuth 2.0 authorization code grant flow reply listening HTTP servers.
|
||||
pub fn stop_listeners() {
|
||||
SERVER_SHUTDOWN.send(()).ok();
|
||||
}
|
||||
|
||||
async fn handle_reply(
|
||||
req: hyper::Request<Incoming>,
|
||||
auth_code_out: &Mutex<Option<String>>,
|
||||
) -> Result<hyper::Response<String>, hyper::http::Error> {
|
||||
if req.method() != hyper::Method::GET {
|
||||
return hyper::Response::builder()
|
||||
.status(hyper::StatusCode::METHOD_NOT_ALLOWED)
|
||||
.header("Allow", "GET")
|
||||
.body("".into());
|
||||
}
|
||||
|
||||
// The authorization code is guaranteed to be sent as a "code" query parameter
|
||||
// in the request URI query string as per RFC 6749 § 4.1.2
|
||||
let auth_code = req.uri().query().and_then(|query_string| {
|
||||
query_string
|
||||
.split('&')
|
||||
.filter_map(|query_pair| query_pair.split_once('='))
|
||||
.find_map(|(key, value)| (key == "code").then_some(value))
|
||||
});
|
||||
|
||||
let response = if let Some(auth_code) = auth_code {
|
||||
*auth_code_out.lock().unwrap() = Some(auth_code.to_string());
|
||||
|
||||
hyper::Response::builder()
|
||||
.status(hyper::StatusCode::OK)
|
||||
.header("Content-Type", "text/html;charset=utf-8")
|
||||
.body(
|
||||
include_str!("auth_code_reply/page.html")
|
||||
.replace("{{title}}", "Success")
|
||||
.replace("{{message}}", "You have successfully signed in! You can close this page now."),
|
||||
)
|
||||
} else {
|
||||
hyper::Response::builder()
|
||||
.status(hyper::StatusCode::BAD_REQUEST)
|
||||
.header("Content-Type", "text/html;charset=utf-8")
|
||||
.body(
|
||||
include_str!("auth_code_reply/page.html")
|
||||
.replace("{{title}}", "Error")
|
||||
.replace("{{message}}", "Authorization code not found. Please try signing in again."),
|
||||
)
|
||||
}?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
1
apps/app/src/api/oauth_utils/auth_code_reply/page.html
Normal file
1
apps/app/src/api/oauth_utils/auth_code_reply/page.html
Normal file
File diff suppressed because one or more lines are too long
3
apps/app/src/api/oauth_utils/mod.rs
Normal file
3
apps/app/src/api/oauth_utils/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
//! Assorted utilities for OAuth 2.0 authorization flows.
|
||||
|
||||
pub mod auth_code_reply;
|
||||
@@ -28,7 +28,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
profile_update_managed_modrinth_version,
|
||||
profile_repair_managed_modrinth,
|
||||
profile_run,
|
||||
profile_run_credentials,
|
||||
profile_kill,
|
||||
profile_edit,
|
||||
profile_edit_icon,
|
||||
@@ -256,22 +255,6 @@ pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
// Run Minecraft using a profile using chosen credentials
|
||||
// Returns the UUID, which can be used to poll
|
||||
// for the actual Child in the state.
|
||||
// invoke('plugin:profile|profile_run_credentials', {path, credentials})')
|
||||
#[tauri::command]
|
||||
pub async fn profile_run_credentials(
|
||||
path: &str,
|
||||
credentials: Credentials,
|
||||
) -> Result<ProcessMetadata> {
|
||||
let process =
|
||||
profile::run_credentials(path, &credentials, &QuickPlayType::None)
|
||||
.await?;
|
||||
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_kill(path: &str) -> Result<()> {
|
||||
profile::kill(path).await?;
|
||||
|
||||
@@ -37,6 +37,7 @@ pub fn get_os() -> OS {
|
||||
let os = OS::MacOS;
|
||||
os
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum OS {
|
||||
|
||||
@@ -5,8 +5,8 @@ use tauri::{AppHandle, Manager, Runtime};
|
||||
use theseus::prelude::ProcessMetadata;
|
||||
use theseus::profile::{QuickPlayType, get_full_path};
|
||||
use theseus::worlds::{
|
||||
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
||||
WorldWithProfile,
|
||||
DisplayStatus, ProtocolVersion, ServerPackStatus, ServerStatus, World,
|
||||
WorldType, WorldWithProfile,
|
||||
};
|
||||
use theseus::{profile, worlds};
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn get_recent_worlds<R: Runtime>(
|
||||
display_statuses.unwrap_or(EnumSet::all()),
|
||||
)
|
||||
.await?;
|
||||
for world in result.iter_mut() {
|
||||
for world in &mut result {
|
||||
adapt_world_icon(&app_handle, &mut world.world);
|
||||
}
|
||||
Ok(result)
|
||||
@@ -55,7 +55,7 @@ pub async fn get_profile_worlds<R: Runtime>(
|
||||
path: &str,
|
||||
) -> Result<Vec<World>> {
|
||||
let mut result = worlds::get_profile_worlds(path).await?;
|
||||
for world in result.iter_mut() {
|
||||
for world in &mut result {
|
||||
adapt_world_icon(&app_handle, world);
|
||||
}
|
||||
Ok(result)
|
||||
@@ -183,14 +183,16 @@ pub async fn remove_server_from_profile(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_profile_protocol_version(path: &str) -> Result<Option<i32>> {
|
||||
pub async fn get_profile_protocol_version(
|
||||
path: &str,
|
||||
) -> Result<Option<ProtocolVersion>> {
|
||||
Ok(worlds::get_profile_protocol_version(path).await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_server_status(
|
||||
address: &str,
|
||||
protocol_version: Option<i32>,
|
||||
protocol_version: Option<ProtocolVersion>,
|
||||
) -> Result<ServerStatus> {
|
||||
Ok(worlds::get_server_status(address, protocol_version).await?)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
||||
manager: &M,
|
||||
) -> InitialPayload {
|
||||
let initial_payload = manager.try_state::<InitialPayload>();
|
||||
let mtx = if let Some(initial_payload) = initial_payload {
|
||||
|
||||
if let Some(initial_payload) = initial_payload {
|
||||
initial_payload.inner().clone()
|
||||
} else {
|
||||
tracing::info!("No initial payload found, creating new");
|
||||
@@ -22,7 +23,5 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
||||
manager.manage(payload.clone());
|
||||
|
||||
payload
|
||||
};
|
||||
|
||||
mtx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ fn main() {
|
||||
let _ = win.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
@@ -197,7 +198,7 @@ fn main() {
|
||||
{
|
||||
let payload = macos::deep_link::get_or_init_payload(app);
|
||||
|
||||
let mtx_copy = payload.payload.clone();
|
||||
let mtx_copy = payload.payload;
|
||||
app.listen("deep-link://new-url", move |url| {
|
||||
let mtx_copy_copy = mtx_copy.clone();
|
||||
let request = url.payload().to_owned();
|
||||
@@ -229,7 +230,6 @@ fn main() {
|
||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||
payload,
|
||||
));
|
||||
dbg!(url);
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
@@ -249,6 +249,7 @@ fn main() {
|
||||
.plugin(api::logs::init())
|
||||
.plugin(api::jre::init())
|
||||
.plugin(api::metadata::init())
|
||||
.plugin(api::minecraft_skins::init())
|
||||
.plugin(api::pack::init())
|
||||
.plugin(api::process::init())
|
||||
.plugin(api::profile::init())
|
||||
@@ -273,22 +274,22 @@ fn main() {
|
||||
|
||||
match app {
|
||||
Ok(app) => {
|
||||
#[allow(unused_variables)]
|
||||
app.run(|app, event| {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
drop((app, event));
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Opened { urls } = event {
|
||||
tracing::info!("Handling webview open {urls:?}");
|
||||
|
||||
let file = urls
|
||||
.into_iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.next();
|
||||
.find_map(|url| url.to_file_path().ok());
|
||||
|
||||
if let Some(file) = file {
|
||||
let payload =
|
||||
macos::deep_link::get_or_init_payload(app);
|
||||
|
||||
let mtx_copy = payload.payload.clone();
|
||||
let mtx_copy = payload.payload;
|
||||
let request = file.to_string_lossy().to_string();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut payload = mtx_copy.lock().await;
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
{
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
"createUpdaterArtifacts": "v1Compatible",
|
||||
"windows": {
|
||||
"signCommand": {
|
||||
"cmd": "jsign",
|
||||
"args": [
|
||||
"sign",
|
||||
"--verbose",
|
||||
"--storetype",
|
||||
"DIGICERTONE",
|
||||
"--keystore",
|
||||
"https://clientauth.one.digicert.com",
|
||||
"--storepass",
|
||||
"env:DIGICERT_ONE_SIGNER_CREDENTIALS",
|
||||
"--tsaurl",
|
||||
"https://timestamp.sectigo.com,http://timestamp.digicert.com",
|
||||
"%1"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"features": ["updater"]
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
"externalBin": [],
|
||||
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.digicert.com",
|
||||
"nsis": {
|
||||
"installMode": "perMachine",
|
||||
"installerHooks": "./nsis/hooks.nsi"
|
||||
@@ -30,7 +27,6 @@
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"linux": {
|
||||
"deb": {
|
||||
@@ -45,7 +41,7 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.9.5",
|
||||
"version": "../app-frontend/package.json",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
@@ -67,6 +63,7 @@
|
||||
"height": 800,
|
||||
"resizable": true,
|
||||
"title": "Modrinth App",
|
||||
"label": "main",
|
||||
"width": 1280,
|
||||
"minHeight": 700,
|
||||
"minWidth": 1100,
|
||||
@@ -90,9 +87,9 @@
|
||||
"capabilities": ["ads", "core", "plugins"],
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:",
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"script-src": "https://*.posthog.com 'self'",
|
||||
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'",
|
||||
|
||||
14
apps/app/turbo.jsonc
Normal file
14
apps/app/turbo.jsonc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "../../node_modules/turbo/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
// Running Clippy and tests on a Tauri application requires
|
||||
// the frontend to be built at least once first
|
||||
"lint": {
|
||||
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "daedalus_client"
|
||||
version = "0.2.2"
|
||||
authors = ["Jai A <jai@modrinth.com>"]
|
||||
edition = "2024"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -28,3 +28,6 @@ tracing-error.workspace = true
|
||||
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
FROM rust:1.86.0 AS build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM rust:1.88.0 AS build
|
||||
|
||||
WORKDIR /usr/src/daedalus
|
||||
COPY . .
|
||||
RUN cargo build --release --package daedalus_client
|
||||
RUN --mount=type=cache,target=/usr/src/daedalus/target \
|
||||
--mount=type=cache,target=/usr/local/cargo/git/db \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
cargo build --release --package daedalus_client
|
||||
|
||||
FROM build AS artifacts
|
||||
|
||||
RUN --mount=type=cache,target=/usr/src/daedalus/target \
|
||||
mkdir /daedalus \
|
||||
&& cp /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-ca-certificates
|
||||
COPY --from=artifacts /daedalus /daedalus
|
||||
|
||||
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||
WORKDIR /daedalus_client
|
||||
|
||||
CMD /daedalus/daedalus_client
|
||||
CMD ["/daedalus/daedalus_client"]
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"name": "@modrinth/daedalus_client",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/daedalus": "workspace:*"
|
||||
|
||||
@@ -52,8 +52,7 @@ pub async fn fetch(
|
||||
if modrinth_version
|
||||
.original_sha1
|
||||
.as_ref()
|
||||
.map(|x| x == &version.sha1)
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|x| x == &version.sha1)
|
||||
{
|
||||
existing_versions.push(modrinth_version);
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"lint": "astro check",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
@@ -18,4 +19,4 @@
|
||||
"starlight-openapi": "^0.14.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
Support: https://support.modrinth.com
|
||||
Status page: https://status.modrinth.com
|
||||
Roadmap: https://roadmap.modrinth.com
|
||||
Blog and newsletter: https://blog.modrinth.com/subscribe?utm_medium=social&utm_source=discord&utm_campaign=welcome
|
||||
Blog and newsletter: https://modrinth.com/news
|
||||
API documentation: https://docs.modrinth.com
|
||||
Modrinth source code: https://github.com/modrinth
|
||||
Help translate Modrinth: https://crowdin.com/project/modrinth
|
||||
|
||||
@@ -19,8 +19,6 @@ From there, you can create the database and perform all database migrations with
|
||||
sqlx database setup
|
||||
```
|
||||
|
||||
Finally, if on Linux, you will need the OpenSSL library. On Debian-based systems, this involves the `pkg-config` and `libssl-dev` packages.
|
||||
|
||||
To enable labrinth to create a project, you need to add two things.
|
||||
|
||||
1. An entry in the `loaders` table.
|
||||
@@ -85,11 +83,10 @@ During development, you might notice that changes made directly to entities in t
|
||||
|
||||
#### CDN options
|
||||
|
||||
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local`, `backblaze`, or `s3`, but defaults to `local`
|
||||
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local` or `s3`, but defaults to `local`
|
||||
|
||||
The Backblaze and S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
||||
`BACKBLAZE_KEY_ID`, `BACKBLAZE_KEY`, `BACKBLAZE_BUCKET_ID`
|
||||
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_BUCKET_NAME`
|
||||
The S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
||||
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_PUBLIC_BUCKET_NAME`, `S3_PRIVATE_BUCKET_NAME`, `S3_USES_PATH_STYLE_BUCKETS`
|
||||
|
||||
#### Search, OAuth, and miscellaneous options
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ BASE_URL=http://127.0.0.1:8000/v2/
|
||||
BROWSER_BASE_URL=http://127.0.0.1:8000/v2/
|
||||
PYRO_BASE_URL=https://staging-archon.modrinth.com
|
||||
PROD_OVERRIDE=true
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"postinstall": "nuxi prepare",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"test": "nuxi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
@@ -37,9 +38,12 @@
|
||||
"@intercom/messenger-js-sdk": "^0.0.14",
|
||||
"@ltd/j-toml": "^1.38.0",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/blog": "workspace:*",
|
||||
"@modrinth/moderation": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"ace-builds": "^1.36.2",
|
||||
@@ -55,10 +59,12 @@
|
||||
"markdown-it": "14.1.0",
|
||||
"pathe": "^1.1.2",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"prettier": "^3.6.2",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"three": "^0.172.0",
|
||||
"@types/three": "^0.172.0",
|
||||
"vue-confetti-explosion": "^1.0.2",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-typed-virtual-list": "^1.0.10",
|
||||
"vue3-ace-editor": "^2.2.4",
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
<svg width="1120" height="116" viewBox="0 0 1120 116" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_9092_23263" fill="white">
|
||||
<path
|
||||
d="M460.704 -89.7906L418.995 -98.1936C415.591 -98.8731 412.786 -95.5784 414.023 -92.3281L429.187 -52.2419C430.194 -49.5896 433.662 -48.9476 435.498 -51.081L463.062 -83.0993C465.103 -85.4707 463.796 -89.1809 460.711 -89.7994L460.704 -89.7906Z" />
|
||||
<path
|
||||
d="M375.343 -163.469L389.792 -123.4C390.965 -120.129 388.124 -116.865 384.731 -117.612L342.9 -126.719C340.131 -127.327 338.986 -130.666 340.823 -132.799L368.386 -164.818C370.428 -167.189 374.284 -166.442 375.351 -163.478L375.343 -163.469Z" />
|
||||
<path
|
||||
d="M263.038 -121.213L274.559 -86.7584C275.398 -84.2678 277.512 -82.397 280.082 -81.8845L360.91 -65.7792C363.48 -65.2665 365.6 -63.437 366.485 -60.9671L394.325 16.7081C395.21 19.1779 397.363 21.0052 399.953 21.4584L435.687 27.8069C438.261 28.2613 440.866 27.3445 442.558 25.3785L498.485 -39.587C500.094 -41.456 500.626 -44.0395 499.897 -46.4205L490.583 -76.5881C490.392 -77.2139 489.995 -78.4479 489.551 -79.861C488.599 -82.849 484.745 -83.6556 482.737 -81.2863C481.871 -80.2812 481.082 -79.3643 480.596 -78.8001L430.703 -21.059C428.062 -17.991 423.057 -18.9759 421.68 -22.8226L395.993 -94.9369C395.064 -97.5367 392.837 -99.4584 390.141 -99.9873L315.122 -114.797C311.119 -115.594 309.418 -120.397 312.05 -123.473L361.73 -181.398C362.171 -181.909 362.966 -182.852 363.868 -183.917C365.861 -186.269 364.49 -189.942 361.417 -190.428C360.039 -190.649 358.786 -190.854 358.073 -190.963L326.901 -195.74C324.442 -196.111 321.976 -195.212 320.359 -193.335L264.433 -128.369C262.74 -126.403 262.211 -123.678 263.047 -121.205L263.038 -121.213Z" />
|
||||
<path
|
||||
d="M268.307 204.958L310.016 213.361C313.42 214.041 316.226 210.746 314.988 207.496L299.825 167.41C298.817 164.757 295.349 164.115 293.513 166.249L265.949 198.267C263.908 200.639 265.215 204.349 268.3 204.967L268.307 204.958Z" />
|
||||
<path
|
||||
d="M353.668 278.637L339.219 238.568C338.046 235.297 340.887 232.033 344.28 232.78L386.111 241.887C388.88 242.495 390.025 245.834 388.189 247.967L360.625 279.986C358.584 282.357 354.727 281.61 353.66 278.646L353.668 278.637Z" />
|
||||
<path
|
||||
d="M465.973 236.381L454.452 201.926C453.613 199.436 451.499 197.565 448.93 197.052L368.101 180.947C365.531 180.434 363.411 178.605 362.526 176.135L334.686 98.4597C333.801 95.9899 331.648 94.1626 329.058 93.7094L293.324 87.3609C290.75 86.9065 288.145 87.8232 286.453 89.7892L230.526 154.755C228.917 156.624 228.385 159.207 229.114 161.588L238.428 191.756C238.619 192.382 239.016 193.616 239.46 195.029C240.412 198.017 244.266 198.823 246.274 196.454C247.14 195.449 247.929 194.532 248.415 193.968L298.308 136.227C300.95 133.159 305.954 134.144 307.331 137.99L333.018 210.105C333.947 212.704 336.174 214.626 338.87 215.155L413.889 229.965C417.892 230.762 419.593 235.565 416.961 238.64L367.281 296.566C366.84 297.077 366.045 298.019 365.143 299.085C363.15 301.436 364.521 305.11 367.594 305.596C368.972 305.817 370.225 306.022 370.938 306.131L402.11 310.908C404.569 311.279 407.035 310.38 408.652 308.502L464.579 243.537C466.271 241.571 466.8 238.846 465.964 236.373L465.973 236.381Z" />
|
||||
<path
|
||||
d="M440.999 55.1022L399.29 46.6992C395.886 46.0197 393.08 49.3145 394.318 52.5647L409.482 92.6509C410.489 95.3032 413.957 95.9452 415.793 93.8118L443.357 61.7935C445.398 59.4221 444.091 55.7119 441.006 55.0934L440.999 55.1022Z" />
|
||||
<path
|
||||
d="M355.638 -18.5761L370.087 21.4924C371.26 24.7638 368.419 28.0279 365.026 27.2811L323.195 18.1735C320.426 17.5658 319.281 14.2267 321.117 12.0934L348.681 -19.925C350.723 -22.2964 354.579 -21.5491 355.646 -18.5849L355.638 -18.5761Z" />
|
||||
<path
|
||||
d="M243.333 23.6798L254.854 58.1344C255.693 60.6251 257.807 62.4958 260.376 63.0083L341.205 79.1136C343.775 79.6263 345.895 81.4558 346.78 83.9257L374.62 161.601C375.505 164.071 377.658 165.898 380.248 166.351L415.982 172.7C418.555 173.154 421.161 172.237 422.853 170.271L478.78 105.306C480.389 103.437 480.921 100.853 480.192 98.4723L470.878 68.3047C470.687 67.6789 470.29 66.4449 469.846 65.0319C468.894 62.0438 465.04 61.2373 463.032 63.6066C462.166 64.6116 461.377 65.5285 460.891 66.0927L410.998 123.834C408.357 126.902 403.352 125.917 401.975 122.07L376.288 49.9559C375.359 47.3562 373.132 45.4345 370.436 44.9055L295.417 30.0955C291.414 29.2991 289.713 24.496 292.345 21.4203L342.025 -36.5052C342.466 -37.0165 343.261 -37.9588 344.163 -39.0243C346.156 -41.3759 344.785 -45.049 341.712 -45.535C340.334 -45.7564 339.081 -45.9615 338.368 -46.07L307.196 -50.8473C304.737 -51.2181 302.271 -50.3195 300.654 -48.4417L244.727 16.5238C243.035 18.4898 242.506 21.2145 243.342 23.6875L243.333 23.6798Z" />
|
||||
<path
|
||||
d="M693.703 -89.7894L651.995 -98.1924C648.59 -98.8719 645.785 -95.5771 647.023 -92.3269L662.186 -52.2407C663.194 -49.5884 666.661 -48.9464 668.498 -51.0798L696.061 -83.0981C698.103 -85.4695 696.795 -89.1797 693.711 -89.7982L693.703 -89.7894Z" />
|
||||
<path
|
||||
d="M608.342 -163.468L622.791 -123.399C623.964 -120.128 621.123 -116.864 617.73 -117.611L575.899 -126.718C573.13 -127.326 571.985 -130.665 573.822 -132.798L601.385 -164.817C603.427 -167.188 607.283 -166.441 608.35 -163.477L608.342 -163.468Z" />
|
||||
<path
|
||||
d="M496.037 -121.212L507.559 -86.7572C508.397 -84.2665 510.511 -82.3958 513.081 -81.8833L593.909 -65.778C596.48 -65.2653 598.599 -63.4358 599.484 -60.9659L627.324 16.7093C628.209 19.1791 630.362 21.0064 632.952 21.4596L668.686 27.8081C671.26 28.2625 673.865 27.3458 675.558 25.3798L731.485 -39.5858C733.094 -41.4548 733.626 -44.0383 732.897 -46.4193L723.582 -76.5869C723.392 -77.2127 722.995 -78.4467 722.55 -79.8598C721.599 -82.8478 717.745 -83.6543 715.736 -81.285C714.871 -80.28 714.081 -79.3631 713.596 -78.7989L663.702 -21.0578C661.061 -17.9897 656.056 -18.9746 654.68 -22.8213L628.992 -94.9357C628.063 -97.5354 625.837 -99.4571 623.141 -99.9861L548.121 -114.796C544.118 -115.593 542.417 -120.396 545.05 -123.471L594.73 -181.397C595.17 -181.908 595.966 -182.85 596.867 -183.916C598.861 -186.267 597.49 -189.941 594.416 -190.427C593.038 -190.648 591.785 -190.853 591.072 -190.962L559.901 -195.739C557.441 -196.11 554.975 -195.211 553.359 -193.333L497.432 -128.368C495.739 -126.402 495.211 -123.677 496.046 -121.204L496.037 -121.212Z" />
|
||||
<path
|
||||
d="M501.308 204.961L543.017 213.364C546.421 214.043 549.227 210.748 547.989 207.498L532.826 167.412C531.818 164.76 528.35 164.118 526.514 166.251L498.95 198.269C496.909 200.641 498.216 204.351 501.301 204.969L501.308 204.961Z" />
|
||||
<path
|
||||
d="M586.669 278.639L572.221 238.57C571.047 235.299 573.888 232.035 577.282 232.782L619.112 241.889C621.882 242.497 623.026 245.836 621.19 247.969L593.626 279.988C591.585 282.359 587.728 281.612 586.662 278.648L586.669 278.639Z" />
|
||||
<path
|
||||
d="M698.974 236.383L687.453 201.928C686.615 199.438 684.501 197.567 681.931 197.054L601.102 180.949C598.532 180.436 596.412 178.607 595.527 176.137L567.687 98.4619C566.802 95.9921 564.649 94.1648 562.059 93.7116L526.325 87.3631C523.752 86.9087 521.146 87.8254 519.454 89.7914L463.527 154.757C461.918 156.626 461.386 159.209 462.115 161.59L471.429 191.758C471.62 192.384 472.017 193.618 472.461 195.031C473.413 198.019 477.267 198.826 479.276 196.456C480.141 195.451 480.93 194.534 481.416 193.97L531.309 136.229C533.951 133.161 538.955 134.146 540.332 137.993L566.02 210.107C566.948 212.707 569.175 214.628 571.871 215.157L646.89 229.967C650.893 230.764 652.594 235.567 649.962 238.643L600.282 296.568C599.842 297.079 599.046 298.022 598.144 299.087C596.151 301.439 597.522 305.112 600.595 305.598C601.973 305.819 603.226 306.024 603.939 306.133L635.111 310.91C637.57 311.281 640.036 310.382 641.653 308.504L697.58 243.539C699.272 241.573 699.801 238.848 698.965 236.375L698.974 236.383Z" />
|
||||
<path
|
||||
d="M674 55.1032L632.291 46.7001C628.887 46.0206 626.082 49.3155 627.319 52.5657L642.483 92.6519C643.49 95.3041 646.958 95.9462 648.795 93.8128L676.358 61.7945C678.4 59.4231 677.092 55.7129 674.008 55.0944L674 55.1032Z" />
|
||||
<path
|
||||
d="M588.639 -18.5751L603.088 21.4934C604.261 24.7648 601.42 28.0289 598.027 27.2821L556.196 18.1745C553.427 17.5667 552.282 14.2277 554.119 12.0943L581.682 -19.924C583.724 -22.2954 587.58 -21.5481 588.647 -18.584L588.639 -18.5751Z" />
|
||||
<path
|
||||
d="M476.334 23.6808L487.856 58.1354C488.694 60.626 490.808 62.4968 493.378 63.0093L574.206 79.1146C576.776 79.6273 578.896 81.4568 579.781 83.9267L607.621 161.602C608.506 164.072 610.659 165.899 613.249 166.352L648.983 172.701C651.557 173.155 654.162 172.238 655.855 170.272L711.781 105.307C713.39 103.438 713.922 100.854 713.193 98.4733L703.879 68.3057C703.688 67.6799 703.291 66.4459 702.847 65.0328C701.896 62.0448 698.041 61.2382 696.033 63.6075C695.168 64.6126 694.378 65.5295 693.893 66.0937L643.999 123.835C641.358 126.903 636.353 125.918 634.977 122.071L609.289 49.9569C608.36 47.3571 606.134 45.4354 603.438 44.9065L528.418 30.0964C524.415 29.3 522.714 24.4969 525.346 21.4212L575.027 -36.5042C575.467 -37.0156 576.262 -37.9578 577.164 -39.0234C579.158 -41.3749 577.786 -45.048 574.713 -45.5341C573.335 -45.7554 572.082 -45.9606 571.369 -46.069L540.198 -50.8463C537.738 -51.2171 535.272 -50.3185 533.656 -48.4407L477.729 16.5248C476.036 18.4908 475.508 21.2154 476.343 23.6885L476.334 23.6808Z" />
|
||||
<path
|
||||
d="M931.704 -89.7906L889.995 -98.1936C886.591 -98.8731 883.785 -95.5784 885.023 -92.3281L900.186 -52.2419C901.194 -49.5896 904.662 -48.9476 906.498 -51.081L934.062 -83.0993C936.103 -85.4707 934.796 -89.1809 931.711 -89.7994L931.704 -89.7906Z" />
|
||||
<path
|
||||
d="M846.343 -163.469L860.791 -123.4C861.964 -120.129 859.124 -116.865 855.73 -117.612L813.9 -126.719C811.13 -127.327 809.986 -130.666 811.822 -132.799L839.386 -164.818C841.427 -167.189 845.284 -166.442 846.35 -163.478L846.343 -163.469Z" />
|
||||
<path
|
||||
d="M734.038 -121.213L745.559 -86.7584C746.397 -84.2678 748.511 -82.397 751.081 -81.8845L831.91 -65.7792C834.48 -65.2665 836.6 -63.437 837.485 -60.9671L865.325 16.7081C866.21 19.1779 868.362 21.0052 870.953 21.4584L906.687 27.8069C909.26 28.2613 911.866 27.3445 913.558 25.3785L969.485 -39.587C971.094 -41.456 971.626 -44.0395 970.897 -46.4205L961.583 -76.5881C961.392 -77.2139 960.995 -78.4479 960.551 -79.861C959.599 -82.849 955.745 -83.6556 953.736 -81.2863C952.871 -80.2812 952.082 -79.3643 951.596 -78.8001L901.703 -21.059C899.061 -17.991 894.057 -18.9759 892.68 -22.8226L866.992 -94.9369C866.064 -97.5367 863.837 -99.4584 861.141 -99.9873L786.122 -114.797C782.119 -115.594 780.418 -120.397 783.05 -123.473L832.73 -181.398C833.17 -181.909 833.966 -182.852 834.868 -183.917C836.861 -186.269 835.49 -189.942 832.417 -190.428C831.039 -190.649 829.786 -190.854 829.073 -190.963L797.901 -195.74C795.442 -196.111 792.976 -195.212 791.359 -193.335L735.432 -128.369C733.74 -126.403 733.211 -123.678 734.047 -121.205L734.038 -121.213Z" />
|
||||
<path
|
||||
d="M739.307 204.957L781.016 213.36C784.42 214.04 787.225 210.745 785.988 207.495L770.824 167.409C769.817 164.756 766.349 164.114 764.513 166.248L736.949 198.266C734.907 200.638 736.215 204.348 739.299 204.966L739.307 204.957Z" />
|
||||
<path
|
||||
d="M824.668 278.636L810.219 238.567C809.046 235.296 811.887 232.032 815.28 232.779L857.111 241.886C859.88 242.494 861.025 245.833 859.188 247.966L831.625 279.985C829.583 282.356 825.727 281.609 824.66 278.645L824.668 278.636Z" />
|
||||
<path
|
||||
d="M936.973 236.38L925.452 201.925C924.613 199.435 922.499 197.564 919.929 197.051L839.101 180.946C836.531 180.433 834.411 178.604 833.526 176.134L805.686 98.4587C804.801 95.9889 802.648 94.1616 800.058 93.7084L764.324 87.3599C761.75 86.9055 759.145 87.8223 757.452 89.7883L701.526 154.754C699.917 156.623 699.385 159.206 700.114 161.587L709.428 191.755C709.619 192.381 710.016 193.615 710.46 195.028C711.411 198.016 715.266 198.822 717.274 196.453C718.139 195.448 718.929 194.531 719.415 193.967L769.308 136.226C771.949 133.158 776.954 134.143 778.331 137.989L804.018 210.104C804.947 212.703 807.174 214.625 809.87 215.154L884.889 229.964C888.892 230.761 890.593 235.564 887.961 238.639L838.281 296.565C837.84 297.076 837.045 298.018 836.143 299.084C834.15 301.436 835.521 305.109 838.594 305.595C839.972 305.816 841.225 306.021 841.938 306.13L873.109 310.907C875.569 311.278 878.035 310.379 879.652 308.501L935.578 243.536C937.271 241.57 937.8 238.845 936.964 236.372L936.973 236.38Z" />
|
||||
<path
|
||||
d="M911.999 55.102L870.29 46.6989C866.886 46.0194 864.08 49.3142 865.318 52.5645L880.481 92.6507C881.489 95.3029 884.957 95.9449 886.793 93.8116L914.357 61.7933C916.398 59.4218 915.091 55.7117 912.006 55.0932L911.999 55.102Z" />
|
||||
<path
|
||||
d="M826.638 -18.5764L841.086 21.4922C842.259 24.7636 839.418 28.0277 836.025 27.2808L794.195 18.1733C791.425 17.5655 790.281 14.2265 792.117 12.0931L819.681 -19.9252C821.722 -22.2966 825.579 -21.5494 826.645 -18.5852L826.638 -18.5764Z" />
|
||||
<path
|
||||
d="M714.333 23.6796L725.854 58.1342C726.692 60.6248 728.806 62.4956 731.376 63.0081L812.205 79.1134C814.775 79.6261 816.895 81.4556 817.78 83.9254L845.62 161.601C846.505 164.07 848.657 165.898 851.248 166.351L886.981 172.699C889.555 173.154 892.161 172.237 893.853 170.271L949.78 105.306C951.389 103.437 951.921 100.853 951.192 98.4721L941.878 68.3045C941.687 67.6787 941.29 66.4447 940.846 65.0316C939.894 62.0436 936.04 61.237 934.031 63.6063C933.166 64.6113 932.377 65.5283 931.891 66.0925L881.997 123.834C879.356 126.902 874.352 125.917 872.975 122.07L847.287 49.9557C846.359 47.3559 844.132 45.4342 841.436 44.9053L766.417 30.0952C762.414 29.2988 760.713 24.4957 763.345 21.42L813.025 -36.5054C813.465 -37.0168 814.261 -37.959 815.163 -39.0246C817.156 -41.3761 815.785 -45.0493 812.712 -45.5353C811.333 -45.7567 810.081 -45.9618 809.368 -46.0702L778.196 -50.8475C775.737 -51.2183 773.271 -50.3197 771.654 -48.4419L715.727 16.5236C714.035 18.4896 713.506 21.2142 714.342 23.6873L714.333 23.6796Z" />
|
||||
<path
|
||||
d="M1164.7 -89.7906L1122.99 -98.1936C1119.59 -98.8731 1116.78 -95.5784 1118.02 -92.3281L1133.19 -52.2419C1134.19 -49.5896 1137.66 -48.9476 1139.5 -51.081L1167.06 -83.0993C1169.1 -85.4707 1167.79 -89.1809 1164.71 -89.7994L1164.7 -89.7906Z" />
|
||||
<path
|
||||
d="M1079.34 -163.469L1093.79 -123.4C1094.96 -120.129 1092.12 -116.865 1088.73 -117.612L1046.9 -126.719C1044.13 -127.327 1042.98 -130.666 1044.82 -132.799L1072.39 -164.818C1074.43 -167.189 1078.28 -166.442 1079.35 -163.478L1079.34 -163.469Z" />
|
||||
<path
|
||||
d="M967.037 -121.213L978.558 -86.7584C979.397 -84.2678 981.511 -82.397 984.081 -81.8845L1064.91 -65.7792C1067.48 -65.2665 1069.6 -63.437 1070.48 -60.9671L1098.32 16.7081C1099.21 19.1779 1101.36 21.0052 1103.95 21.4584L1139.69 27.8069C1142.26 28.2613 1144.87 27.3445 1146.56 25.3785L1202.48 -39.587C1204.09 -41.456 1204.63 -44.0395 1203.9 -46.4205L1194.58 -76.5881C1194.39 -77.2139 1193.99 -78.4479 1193.55 -79.861C1192.6 -82.849 1188.74 -83.6556 1186.74 -81.2863C1185.87 -80.2812 1185.08 -79.3643 1184.6 -78.8001L1134.7 -21.059C1132.06 -17.991 1127.06 -18.9759 1125.68 -22.8226L1099.99 -94.9369C1099.06 -97.5367 1096.84 -99.4584 1094.14 -99.9873L1019.12 -114.797C1015.12 -115.594 1013.42 -120.397 1016.05 -123.473L1065.73 -181.398C1066.17 -181.909 1066.97 -182.852 1067.87 -183.917C1069.86 -186.269 1068.49 -189.942 1065.42 -190.428C1064.04 -190.649 1062.79 -190.854 1062.07 -190.963L1030.9 -195.74C1028.44 -196.111 1025.97 -195.212 1024.36 -193.335L968.432 -128.369C966.739 -126.403 966.21 -123.678 967.046 -121.205L967.037 -121.213Z" />
|
||||
<path
|
||||
d="M972.308 204.959L1014.02 213.362C1017.42 214.042 1020.23 210.747 1018.99 207.497L1003.83 167.411C1002.82 164.758 999.35 164.116 997.514 166.25L969.95 198.268C967.908 200.639 969.216 204.35 972.3 204.968L972.308 204.959Z" />
|
||||
<path
|
||||
d="M1057.67 278.638L1043.22 238.569C1042.05 235.298 1044.89 232.034 1048.28 232.78L1090.11 241.888C1092.88 242.496 1094.03 245.835 1092.19 247.968L1064.63 279.987C1062.58 282.358 1058.73 281.611 1057.66 278.647L1057.67 278.638Z" />
|
||||
<path
|
||||
d="M1169.97 236.382L1158.45 201.927C1157.61 199.437 1155.5 197.566 1152.93 197.053L1072.1 180.948C1069.53 180.435 1067.41 178.606 1066.53 176.136L1038.69 98.4607C1037.8 95.9908 1035.65 94.1636 1033.06 93.7104L997.325 87.3618C994.751 86.9075 992.146 87.8242 990.453 89.7902L934.527 154.756C932.918 156.625 932.386 159.208 933.115 161.589L942.429 191.757C942.62 192.383 943.017 193.617 943.461 195.03C944.413 198.018 948.267 198.824 950.275 196.455C951.14 195.45 951.93 194.533 952.416 193.969L1002.31 136.228C1004.95 133.16 1009.95 134.145 1011.33 137.991L1037.02 210.106C1037.95 212.705 1040.17 214.627 1042.87 215.156L1117.89 229.966C1121.89 230.763 1123.59 235.566 1120.96 238.641L1071.28 296.567C1070.84 297.078 1070.05 298.02 1069.14 299.086C1067.15 301.437 1068.52 305.111 1071.6 305.597C1072.97 305.818 1074.23 306.023 1074.94 306.132L1106.11 310.909C1108.57 311.28 1111.04 310.381 1112.65 308.503L1168.58 243.538C1170.27 241.572 1170.8 238.847 1169.96 236.374L1169.97 236.382Z" />
|
||||
<path
|
||||
d="M1145 55.102L1103.29 46.6989C1099.89 46.0194 1097.08 49.3142 1098.32 52.5645L1113.48 92.6507C1114.49 95.3029 1117.96 95.9449 1119.79 93.8116L1147.36 61.7933C1149.4 59.4218 1148.09 55.7117 1145.01 55.0932L1145 55.102Z" />
|
||||
<path
|
||||
d="M1059.64 -18.5764L1074.09 21.4922C1075.26 24.7636 1072.42 28.0277 1069.03 27.2808L1027.2 18.1733C1024.43 17.5655 1023.28 14.2265 1025.12 12.0931L1052.68 -19.9252C1054.72 -22.2966 1058.58 -21.5494 1059.65 -18.5852L1059.64 -18.5764Z" />
|
||||
<path
|
||||
d="M947.334 23.6796L958.855 58.1342C959.694 60.6248 961.808 62.4956 964.377 63.0081L1045.21 79.1134C1047.78 79.6261 1049.9 81.4556 1050.78 83.9254L1078.62 161.601C1079.51 164.07 1081.66 165.898 1084.25 166.351L1119.98 172.699C1122.56 173.154 1125.16 172.237 1126.85 170.271L1182.78 105.306C1184.39 103.437 1184.92 100.853 1184.19 98.4721L1174.88 68.3045C1174.69 67.6787 1174.29 66.4447 1173.85 65.0316C1172.9 62.0436 1169.04 61.237 1167.03 63.6063C1166.17 64.6113 1165.38 65.5283 1164.89 66.0925L1115 123.834C1112.36 126.902 1107.35 125.917 1105.98 122.07L1080.29 49.9557C1079.36 47.3559 1077.13 45.4342 1074.44 44.9053L999.418 30.0952C995.415 29.2988 993.714 24.4957 996.346 21.42L1046.03 -36.5054C1046.47 -37.0168 1047.26 -37.959 1048.16 -39.0246C1050.16 -41.3761 1048.79 -45.0493 1045.71 -45.5353C1044.33 -45.7567 1043.08 -45.9618 1042.37 -46.0702L1011.2 -50.8475C1008.74 -51.2183 1006.27 -50.3197 1004.66 -48.4419L948.728 16.5236C947.036 18.4896 946.507 21.2142 947.343 23.6873L947.334 23.6796Z" />
|
||||
<path
|
||||
d="M-10.2954 -89.7842L-52.0041 -98.1873C-55.4084 -98.8668 -58.2138 -95.572 -56.976 -92.3217L-41.8127 -52.2355C-40.805 -49.5833 -37.3374 -48.9413 -35.5009 -51.0746L-7.93733 -83.093C-5.89584 -85.4644 -7.20342 -89.1745 -10.2878 -89.7931L-10.2954 -89.7842Z" />
|
||||
<path
|
||||
d="M-95.6563 -163.463L-81.2076 -123.394C-80.0346 -120.123 -82.8755 -116.859 -86.2686 -117.605L-128.099 -126.713C-130.869 -127.321 -132.013 -130.66 -130.177 -132.793L-102.613 -164.811C-100.572 -167.183 -96.7151 -166.436 -95.6487 -163.471L-95.6563 -163.463Z" />
|
||||
<path
|
||||
d="M-207.961 -121.207L-196.44 -86.7521C-195.602 -84.2614 -193.488 -82.3906 -190.918 -81.8782L-110.089 -65.7728C-107.519 -65.2602 -105.399 -63.4306 -104.514 -60.9608L-76.6744 16.7144C-75.7891 19.1843 -73.6365 21.0115 -71.0462 21.4648L-35.3125 27.8132C-32.7387 28.2676 -30.1333 27.3509 -28.4408 25.3849L27.486 -39.5807C29.0949 -41.4496 29.627 -44.0332 28.898 -46.4142L19.5838 -76.5818C19.3929 -77.2076 18.996 -78.4415 18.5516 -79.8546C17.6002 -82.8427 13.746 -83.6492 11.7374 -81.2799C10.8722 -80.2749 10.0828 -79.358 9.59708 -78.7937L-40.2965 -21.0526C-42.9377 -17.9846 -47.9423 -18.9695 -49.3189 -22.8162L-75.0067 -94.9306C-75.9355 -97.5303 -78.1619 -99.452 -80.8578 -99.9809L-155.877 -114.791C-159.88 -115.587 -161.581 -120.391 -158.949 -123.466L-109.269 -181.392C-108.829 -181.903 -108.033 -182.845 -107.131 -183.911C-105.138 -186.262 -106.509 -189.935 -109.582 -190.422C-110.961 -190.643 -112.213 -190.848 -112.926 -190.956L-144.098 -195.734C-146.557 -196.105 -149.023 -195.206 -150.64 -193.328L-206.567 -128.363C-208.259 -126.397 -208.788 -123.672 -207.952 -121.199L-207.961 -121.207Z" />
|
||||
<path
|
||||
d="M-202.692 204.964L-160.984 213.367C-157.579 214.046 -154.774 210.752 -156.012 207.501L-171.175 167.415C-172.183 164.763 -175.65 164.121 -177.487 166.254L-205.05 198.272C-207.092 200.644 -205.784 204.354 -202.7 204.973L-202.692 204.964Z" />
|
||||
<path
|
||||
d="M-117.331 278.642L-131.78 238.574C-132.953 235.302 -130.112 232.038 -126.719 232.785L-84.8883 241.892C-82.119 242.5 -80.9743 245.839 -82.8109 247.973L-110.374 279.991C-112.416 282.362 -116.273 281.615 -117.339 278.651L-117.331 278.642Z" />
|
||||
<path
|
||||
d="M-5.0264 236.386L-16.5478 201.932C-17.386 199.441 -19.5 197.57 -22.0699 197.058L-102.898 180.952C-105.469 180.44 -107.588 178.61 -108.474 176.14L-136.313 98.4651C-137.198 95.9952 -139.351 94.168 -141.941 93.7148L-177.675 87.3662C-180.249 86.9119 -182.854 87.8286 -184.547 89.7946L-240.474 154.76C-242.083 156.629 -242.615 159.213 -241.886 161.594L-232.571 191.761C-232.381 192.387 -231.984 193.621 -231.539 195.034C-230.588 198.022 -226.734 198.829 -224.725 196.459C-223.86 195.454 -223.07 194.537 -222.585 193.973L-172.691 136.232C-170.05 133.164 -165.045 134.149 -163.669 137.996L-137.981 210.11C-137.052 212.71 -134.826 214.631 -132.13 215.16L-57.1103 229.971C-53.1076 230.767 -51.4064 235.57 -54.0387 238.646L-103.719 296.571C-104.159 297.083 -104.955 298.025 -105.856 299.09C-107.85 301.442 -106.479 305.115 -103.405 305.601C-102.027 305.822 -100.775 306.028 -100.061 306.136L-68.8898 310.913C-66.4306 311.284 -63.9642 310.385 -62.3477 308.508L-6.42088 243.542C-4.72842 241.576 -4.19972 238.852 -5.03531 236.378L-5.0264 236.386Z" />
|
||||
<path
|
||||
d="M-30.0004 55.1083L-71.7092 46.7053C-75.1135 46.0258 -77.9188 49.3206 -76.6811 52.5708L-61.5178 92.657C-60.5101 95.3093 -57.0425 95.9513 -55.206 93.8179L-27.6424 61.7996C-25.6009 59.4282 -26.9085 55.7181 -29.9929 55.0995L-30.0004 55.1083Z" />
|
||||
<path
|
||||
d="M-115.361 -18.57L-100.913 21.4985C-99.7397 24.7699 -102.581 28.034 -105.974 27.2872L-147.804 18.1796C-150.574 17.5719 -151.718 14.2328 -149.882 12.0995L-122.318 -19.9189C-120.277 -22.2903 -116.42 -21.543 -115.354 -18.5788L-115.361 -18.57Z" />
|
||||
<path
|
||||
d="M-227.666 23.6859L-216.145 58.1405C-215.307 60.6312 -213.193 62.5019 -210.623 63.0144L-129.794 79.1198C-127.224 79.6324 -125.104 81.4619 -124.219 83.9318L-96.3795 161.607C-95.4942 164.077 -93.3416 165.904 -90.7513 166.357L-55.0176 172.706C-52.4438 173.16 -49.8384 172.243 -48.1459 170.277L7.78091 105.312C9.38986 103.443 9.9219 100.859 9.19295 98.4784L-0.121262 68.3108C-0.312151 67.685 -0.709083 66.451 -1.15346 65.038C-2.10493 62.0499 -5.95909 61.2434 -7.96773 63.6127C-8.83292 64.6177 -9.62228 65.5346 -10.108 66.0988L-60.0016 123.84C-62.6428 126.908 -67.6474 125.923 -69.0239 122.076L-94.7117 49.962C-95.6405 47.3623 -97.8669 45.4406 -100.563 44.9116L-175.582 30.1016C-179.585 29.3052 -181.286 24.5021 -178.654 21.4264L-128.974 -36.4991C-128.534 -37.0104 -127.738 -37.9527 -126.836 -39.0182C-124.843 -41.3698 -126.214 -45.0429 -129.288 -45.5289C-130.666 -45.7503 -131.918 -45.9554 -132.631 -46.0639L-163.803 -50.8412C-166.262 -51.212 -168.728 -50.3134 -170.345 -48.4356L-226.272 16.5299C-227.964 18.4959 -228.493 21.2206 -227.657 23.6936L-227.666 23.6859Z" />
|
||||
<path
|
||||
d="M222.706 -89.7842L180.997 -98.1873C177.593 -98.8668 174.787 -95.572 176.025 -92.3217L191.188 -52.2355C192.196 -49.5833 195.663 -48.9413 197.5 -51.0746L225.064 -83.093C227.105 -85.4644 225.798 -89.1745 222.713 -89.7931L222.706 -89.7842Z" />
|
||||
<path
|
||||
d="M137.345 -163.463L151.793 -123.394C152.966 -120.123 150.125 -116.859 146.732 -117.605L104.902 -126.713C102.132 -127.321 100.988 -130.66 102.824 -132.793L130.388 -164.811C132.429 -167.183 136.286 -166.436 137.352 -163.471L137.345 -163.463Z" />
|
||||
<path
|
||||
d="M25.0397 -121.207L36.5611 -86.7521C37.3993 -84.2614 39.5133 -82.3906 42.0832 -81.8782L122.912 -65.7728C125.482 -65.2602 127.602 -63.4306 128.487 -60.9608L156.327 16.7144C157.212 19.1843 159.364 21.0115 161.955 21.4648L197.688 27.8132C200.262 28.2676 202.868 27.3509 204.56 25.3849L260.487 -39.5807C262.096 -41.4496 262.628 -44.0332 261.899 -46.4142L252.585 -76.5818C252.394 -77.2076 251.997 -78.4415 251.553 -79.8546C250.601 -82.8427 246.747 -83.6492 244.738 -81.2799C243.873 -80.2749 243.084 -79.358 242.598 -78.7937L192.704 -21.0526C190.063 -17.9846 185.059 -18.9695 183.682 -22.8162L157.994 -94.9306C157.066 -97.5303 154.839 -99.452 152.143 -99.9809L77.1236 -114.791C73.1209 -115.587 71.4197 -120.391 74.052 -123.466L123.732 -181.392C124.172 -181.903 124.968 -182.845 125.87 -183.911C127.863 -186.262 126.492 -189.935 123.418 -190.422C122.04 -190.643 120.788 -190.848 120.075 -190.956L88.9031 -195.734C86.4439 -196.105 83.9776 -195.206 82.361 -193.328L26.4342 -128.363C24.7417 -126.397 24.213 -123.672 25.0486 -121.199L25.0397 -121.207Z" />
|
||||
<path
|
||||
d="M30.3106 204.966L72.0193 213.369C75.4237 214.048 78.229 210.753 76.9913 207.503L61.828 167.417C60.8203 164.765 57.3527 164.123 55.5162 166.256L27.9526 198.274C25.9111 200.646 27.2187 204.356 30.303 204.975L30.3106 204.966Z" />
|
||||
<path
|
||||
d="M115.672 278.644L101.223 238.576C100.05 235.304 102.891 232.04 106.284 232.787L148.115 241.894C150.884 242.502 152.029 245.841 150.192 247.975L122.628 279.993C120.587 282.364 116.73 281.617 115.664 278.653L115.672 278.644Z" />
|
||||
<path
|
||||
d="M227.977 236.388L216.455 201.934C215.617 199.443 213.503 197.572 210.933 197.06L130.104 180.954C127.534 180.442 125.415 178.612 124.529 176.142L96.6897 98.467C95.8044 95.9972 93.6518 94.17 91.0615 93.7167L55.3278 87.3682C52.754 86.9138 50.1486 87.8306 48.4561 89.7966L-7.47073 154.762C-9.07968 156.631 -9.61172 159.215 -8.88277 161.596L0.431442 191.763C0.622332 192.389 1.01926 193.623 1.46364 195.036C2.41511 198.024 6.26927 198.831 8.27791 196.461C9.1431 195.456 9.93247 194.539 10.4182 193.975L60.3118 136.234C62.953 133.166 67.9575 134.151 69.3341 137.998L95.0219 210.112C95.9507 212.712 98.1771 214.633 100.873 215.162L175.893 229.972C179.895 230.769 181.597 235.572 178.964 238.648L129.284 296.573C128.844 297.084 128.048 298.027 127.147 299.092C125.153 301.444 126.524 305.117 129.598 305.603C130.976 305.824 132.228 306.029 132.942 306.138L164.113 310.915C166.572 311.286 169.039 310.387 170.655 308.51L226.582 243.544C228.274 241.578 228.803 238.853 227.968 236.38L227.977 236.388Z" />
|
||||
<path
|
||||
d="M203.002 55.1083L161.294 46.7053C157.889 46.0258 155.084 49.3206 156.322 52.5708L171.485 92.657C172.493 95.3093 175.96 95.9513 177.797 93.8179L205.361 61.7996C207.402 59.4282 206.094 55.7181 203.01 55.0995L203.002 55.1083Z" />
|
||||
<path
|
||||
d="M117.642 -18.57L132.09 21.4985C133.263 24.7699 130.422 28.034 127.029 27.2872L85.1985 18.1796C82.4292 17.5719 81.2845 14.2328 83.121 12.0995L110.685 -19.9189C112.726 -22.2903 116.583 -21.543 117.649 -18.5788L117.642 -18.57Z" />
|
||||
<path
|
||||
d="M5.33659 23.6859L16.858 58.1405C17.6961 60.6312 19.8102 62.5019 22.3801 63.0144L103.209 79.1198C105.779 79.6324 107.898 81.4619 108.784 83.9318L136.623 161.607C137.509 164.077 139.661 165.904 142.252 166.357L177.985 172.706C180.559 173.16 183.165 172.243 184.857 170.277L240.784 105.312C242.393 103.443 242.925 100.859 242.196 98.4784L232.882 68.3108C232.691 67.685 232.294 66.451 231.849 65.038C230.898 62.0499 227.044 61.2434 225.035 63.6127C224.17 64.6177 223.381 65.5346 222.895 66.0988L173.001 123.84C170.36 126.908 165.356 125.923 163.979 122.076L138.291 49.962C137.362 47.3623 135.136 45.4406 132.44 44.9116L57.4205 30.1016C53.4178 29.3052 51.7166 24.5021 54.3488 21.4264L104.029 -36.4991C104.469 -37.0104 105.265 -37.9527 106.167 -39.0182C108.16 -41.3698 106.789 -45.0429 103.715 -45.5289C102.337 -45.7503 101.085 -45.9554 100.372 -46.0639L69.2 -50.8412C66.7408 -51.212 64.2744 -50.3134 62.6579 -48.4356L6.73106 16.5299C5.0386 18.4959 4.5099 21.2206 5.34549 23.6936L5.33659 23.6859Z" />
|
||||
</mask>
|
||||
<path
|
||||
d="M460.704 -89.7906L418.995 -98.1936C415.591 -98.8731 412.786 -95.5784 414.023 -92.3281L429.187 -52.2419C430.194 -49.5896 433.662 -48.9476 435.498 -51.081L463.062 -83.0993C465.103 -85.4707 463.796 -89.1809 460.711 -89.7994L460.704 -89.7906Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M375.343 -163.469L389.792 -123.4C390.965 -120.129 388.124 -116.865 384.731 -117.612L342.9 -126.719C340.131 -127.327 338.986 -130.666 340.823 -132.799L368.386 -164.818C370.428 -167.189 374.284 -166.442 375.351 -163.478L375.343 -163.469Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M263.038 -121.213L274.559 -86.7584C275.398 -84.2678 277.512 -82.397 280.082 -81.8845L360.91 -65.7792C363.48 -65.2665 365.6 -63.437 366.485 -60.9671L394.325 16.7081C395.21 19.1779 397.363 21.0052 399.953 21.4584L435.687 27.8069C438.261 28.2613 440.866 27.3445 442.558 25.3785L498.485 -39.587C500.094 -41.456 500.626 -44.0395 499.897 -46.4205L490.583 -76.5881C490.392 -77.2139 489.995 -78.4479 489.551 -79.861C488.599 -82.849 484.745 -83.6556 482.737 -81.2863C481.871 -80.2812 481.082 -79.3643 480.596 -78.8001L430.703 -21.059C428.062 -17.991 423.057 -18.9759 421.68 -22.8226L395.993 -94.9369C395.064 -97.5367 392.837 -99.4584 390.141 -99.9873L315.122 -114.797C311.119 -115.594 309.418 -120.397 312.05 -123.473L361.73 -181.398C362.171 -181.909 362.966 -182.852 363.868 -183.917C365.861 -186.269 364.49 -189.942 361.417 -190.428C360.039 -190.649 358.786 -190.854 358.073 -190.963L326.901 -195.74C324.442 -196.111 321.976 -195.212 320.359 -193.335L264.433 -128.369C262.74 -126.403 262.211 -123.678 263.047 -121.205L263.038 -121.213Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M268.307 204.958L310.016 213.361C313.42 214.041 316.226 210.746 314.988 207.496L299.825 167.41C298.817 164.757 295.349 164.115 293.513 166.249L265.949 198.267C263.908 200.639 265.215 204.349 268.3 204.967L268.307 204.958Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M353.668 278.637L339.219 238.568C338.046 235.297 340.887 232.033 344.28 232.78L386.111 241.887C388.88 242.495 390.025 245.834 388.189 247.967L360.625 279.986C358.584 282.357 354.727 281.61 353.66 278.646L353.668 278.637Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M465.973 236.381L454.452 201.926C453.613 199.436 451.499 197.565 448.93 197.052L368.101 180.947C365.531 180.434 363.411 178.605 362.526 176.135L334.686 98.4597C333.801 95.9899 331.648 94.1626 329.058 93.7094L293.324 87.3609C290.75 86.9065 288.145 87.8232 286.453 89.7892L230.526 154.755C228.917 156.624 228.385 159.207 229.114 161.588L238.428 191.756C238.619 192.382 239.016 193.616 239.46 195.029C240.412 198.017 244.266 198.823 246.274 196.454C247.14 195.449 247.929 194.532 248.415 193.968L298.308 136.227C300.95 133.159 305.954 134.144 307.331 137.99L333.018 210.105C333.947 212.704 336.174 214.626 338.87 215.155L413.889 229.965C417.892 230.762 419.593 235.565 416.961 238.64L367.281 296.566C366.84 297.077 366.045 298.019 365.143 299.085C363.15 301.436 364.521 305.11 367.594 305.596C368.972 305.817 370.225 306.022 370.938 306.131L402.11 310.908C404.569 311.279 407.035 310.38 408.652 308.502L464.579 243.537C466.271 241.571 466.8 238.846 465.964 236.373L465.973 236.381Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M440.999 55.1022L399.29 46.6992C395.886 46.0197 393.08 49.3145 394.318 52.5647L409.482 92.6509C410.489 95.3032 413.957 95.9452 415.793 93.8118L443.357 61.7935C445.398 59.4221 444.091 55.7119 441.006 55.0934L440.999 55.1022Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M355.638 -18.5761L370.087 21.4924C371.26 24.7638 368.419 28.0279 365.026 27.2811L323.195 18.1735C320.426 17.5658 319.281 14.2267 321.117 12.0934L348.681 -19.925C350.723 -22.2964 354.579 -21.5491 355.646 -18.5849L355.638 -18.5761Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M243.333 23.6798L254.854 58.1344C255.693 60.6251 257.807 62.4958 260.376 63.0083L341.205 79.1136C343.775 79.6263 345.895 81.4558 346.78 83.9257L374.62 161.601C375.505 164.071 377.658 165.898 380.248 166.351L415.982 172.7C418.555 173.154 421.161 172.237 422.853 170.271L478.78 105.306C480.389 103.437 480.921 100.853 480.192 98.4723L470.878 68.3047C470.687 67.6789 470.29 66.4449 469.846 65.0319C468.894 62.0438 465.04 61.2373 463.032 63.6066C462.166 64.6116 461.377 65.5285 460.891 66.0927L410.998 123.834C408.357 126.902 403.352 125.917 401.975 122.07L376.288 49.9559C375.359 47.3562 373.132 45.4345 370.436 44.9055L295.417 30.0955C291.414 29.2991 289.713 24.496 292.345 21.4203L342.025 -36.5052C342.466 -37.0165 343.261 -37.9588 344.163 -39.0243C346.156 -41.3759 344.785 -45.049 341.712 -45.535C340.334 -45.7564 339.081 -45.9615 338.368 -46.07L307.196 -50.8473C304.737 -51.2181 302.271 -50.3195 300.654 -48.4417L244.727 16.5238C243.035 18.4898 242.506 21.2145 243.342 23.6875L243.333 23.6798Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M693.703 -89.7894L651.995 -98.1924C648.59 -98.8719 645.785 -95.5771 647.023 -92.3269L662.186 -52.2407C663.194 -49.5884 666.661 -48.9464 668.498 -51.0798L696.061 -83.0981C698.103 -85.4695 696.795 -89.1797 693.711 -89.7982L693.703 -89.7894Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M608.342 -163.468L622.791 -123.399C623.964 -120.128 621.123 -116.864 617.73 -117.611L575.899 -126.718C573.13 -127.326 571.985 -130.665 573.822 -132.798L601.385 -164.817C603.427 -167.188 607.283 -166.441 608.35 -163.477L608.342 -163.468Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M496.037 -121.212L507.559 -86.7572C508.397 -84.2665 510.511 -82.3958 513.081 -81.8833L593.909 -65.778C596.48 -65.2653 598.599 -63.4358 599.484 -60.9659L627.324 16.7093C628.209 19.1791 630.362 21.0064 632.952 21.4596L668.686 27.8081C671.26 28.2625 673.865 27.3458 675.558 25.3798L731.485 -39.5858C733.094 -41.4548 733.626 -44.0383 732.897 -46.4193L723.582 -76.5869C723.392 -77.2127 722.995 -78.4467 722.55 -79.8598C721.599 -82.8478 717.745 -83.6543 715.736 -81.285C714.871 -80.28 714.081 -79.3631 713.596 -78.7989L663.702 -21.0578C661.061 -17.9897 656.056 -18.9746 654.68 -22.8213L628.992 -94.9357C628.063 -97.5354 625.837 -99.4571 623.141 -99.9861L548.121 -114.796C544.118 -115.593 542.417 -120.396 545.05 -123.471L594.73 -181.397C595.17 -181.908 595.966 -182.85 596.867 -183.916C598.861 -186.267 597.49 -189.941 594.416 -190.427C593.038 -190.648 591.785 -190.853 591.072 -190.962L559.901 -195.739C557.441 -196.11 554.975 -195.211 553.359 -193.333L497.432 -128.368C495.739 -126.402 495.211 -123.677 496.046 -121.204L496.037 -121.212Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M501.308 204.961L543.017 213.364C546.421 214.043 549.227 210.748 547.989 207.498L532.826 167.412C531.818 164.76 528.35 164.118 526.514 166.251L498.95 198.269C496.909 200.641 498.216 204.351 501.301 204.969L501.308 204.961Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M586.669 278.639L572.221 238.57C571.047 235.299 573.888 232.035 577.282 232.782L619.112 241.889C621.882 242.497 623.026 245.836 621.19 247.969L593.626 279.988C591.585 282.359 587.728 281.612 586.662 278.648L586.669 278.639Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M698.974 236.383L687.453 201.928C686.615 199.438 684.501 197.567 681.931 197.054L601.102 180.949C598.532 180.436 596.412 178.607 595.527 176.137L567.687 98.4619C566.802 95.9921 564.649 94.1648 562.059 93.7116L526.325 87.3631C523.752 86.9087 521.146 87.8254 519.454 89.7914L463.527 154.757C461.918 156.626 461.386 159.209 462.115 161.59L471.429 191.758C471.62 192.384 472.017 193.618 472.461 195.031C473.413 198.019 477.267 198.826 479.276 196.456C480.141 195.451 480.93 194.534 481.416 193.97L531.309 136.229C533.951 133.161 538.955 134.146 540.332 137.993L566.02 210.107C566.948 212.707 569.175 214.628 571.871 215.157L646.89 229.967C650.893 230.764 652.594 235.567 649.962 238.643L600.282 296.568C599.842 297.079 599.046 298.022 598.144 299.087C596.151 301.439 597.522 305.112 600.595 305.598C601.973 305.819 603.226 306.024 603.939 306.133L635.111 310.91C637.57 311.281 640.036 310.382 641.653 308.504L697.58 243.539C699.272 241.573 699.801 238.848 698.965 236.375L698.974 236.383Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M674 55.1032L632.291 46.7001C628.887 46.0206 626.082 49.3155 627.319 52.5657L642.483 92.6519C643.49 95.3041 646.958 95.9462 648.795 93.8128L676.358 61.7945C678.4 59.4231 677.092 55.7129 674.008 55.0944L674 55.1032Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M588.639 -18.5751L603.088 21.4934C604.261 24.7648 601.42 28.0289 598.027 27.2821L556.196 18.1745C553.427 17.5667 552.282 14.2277 554.119 12.0943L581.682 -19.924C583.724 -22.2954 587.58 -21.5481 588.647 -18.584L588.639 -18.5751Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M476.334 23.6808L487.856 58.1354C488.694 60.626 490.808 62.4968 493.378 63.0093L574.206 79.1146C576.776 79.6273 578.896 81.4568 579.781 83.9267L607.621 161.602C608.506 164.072 610.659 165.899 613.249 166.352L648.983 172.701C651.557 173.155 654.162 172.238 655.855 170.272L711.781 105.307C713.39 103.438 713.922 100.854 713.193 98.4733L703.879 68.3057C703.688 67.6799 703.291 66.4459 702.847 65.0328C701.896 62.0448 698.041 61.2382 696.033 63.6075C695.168 64.6126 694.378 65.5295 693.893 66.0937L643.999 123.835C641.358 126.903 636.353 125.918 634.977 122.071L609.289 49.9569C608.36 47.3571 606.134 45.4354 603.438 44.9065L528.418 30.0964C524.415 29.3 522.714 24.4969 525.346 21.4212L575.027 -36.5042C575.467 -37.0156 576.262 -37.9578 577.164 -39.0234C579.158 -41.3749 577.786 -45.048 574.713 -45.5341C573.335 -45.7554 572.082 -45.9606 571.369 -46.069L540.198 -50.8463C537.738 -51.2171 535.272 -50.3185 533.656 -48.4407L477.729 16.5248C476.036 18.4908 475.508 21.2154 476.343 23.6885L476.334 23.6808Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M931.704 -89.7906L889.995 -98.1936C886.591 -98.8731 883.785 -95.5784 885.023 -92.3281L900.186 -52.2419C901.194 -49.5896 904.662 -48.9476 906.498 -51.081L934.062 -83.0993C936.103 -85.4707 934.796 -89.1809 931.711 -89.7994L931.704 -89.7906Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M846.343 -163.469L860.791 -123.4C861.964 -120.129 859.124 -116.865 855.73 -117.612L813.9 -126.719C811.13 -127.327 809.986 -130.666 811.822 -132.799L839.386 -164.818C841.427 -167.189 845.284 -166.442 846.35 -163.478L846.343 -163.469Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M734.038 -121.213L745.559 -86.7584C746.397 -84.2678 748.511 -82.397 751.081 -81.8845L831.91 -65.7792C834.48 -65.2665 836.6 -63.437 837.485 -60.9671L865.325 16.7081C866.21 19.1779 868.362 21.0052 870.953 21.4584L906.687 27.8069C909.26 28.2613 911.866 27.3445 913.558 25.3785L969.485 -39.587C971.094 -41.456 971.626 -44.0395 970.897 -46.4205L961.583 -76.5881C961.392 -77.2139 960.995 -78.4479 960.551 -79.861C959.599 -82.849 955.745 -83.6556 953.736 -81.2863C952.871 -80.2812 952.082 -79.3643 951.596 -78.8001L901.703 -21.059C899.061 -17.991 894.057 -18.9759 892.68 -22.8226L866.992 -94.9369C866.064 -97.5367 863.837 -99.4584 861.141 -99.9873L786.122 -114.797C782.119 -115.594 780.418 -120.397 783.05 -123.473L832.73 -181.398C833.17 -181.909 833.966 -182.852 834.868 -183.917C836.861 -186.269 835.49 -189.942 832.417 -190.428C831.039 -190.649 829.786 -190.854 829.073 -190.963L797.901 -195.74C795.442 -196.111 792.976 -195.212 791.359 -193.335L735.432 -128.369C733.74 -126.403 733.211 -123.678 734.047 -121.205L734.038 -121.213Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M739.307 204.957L781.016 213.36C784.42 214.04 787.225 210.745 785.988 207.495L770.824 167.409C769.817 164.756 766.349 164.114 764.513 166.248L736.949 198.266C734.907 200.638 736.215 204.348 739.299 204.966L739.307 204.957Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M824.668 278.636L810.219 238.567C809.046 235.296 811.887 232.032 815.28 232.779L857.111 241.886C859.88 242.494 861.025 245.833 859.188 247.966L831.625 279.985C829.583 282.356 825.727 281.609 824.66 278.645L824.668 278.636Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M936.973 236.38L925.452 201.925C924.613 199.435 922.499 197.564 919.929 197.051L839.101 180.946C836.531 180.433 834.411 178.604 833.526 176.134L805.686 98.4587C804.801 95.9889 802.648 94.1616 800.058 93.7084L764.324 87.3599C761.75 86.9055 759.145 87.8223 757.452 89.7883L701.526 154.754C699.917 156.623 699.385 159.206 700.114 161.587L709.428 191.755C709.619 192.381 710.016 193.615 710.46 195.028C711.411 198.016 715.266 198.822 717.274 196.453C718.139 195.448 718.929 194.531 719.415 193.967L769.308 136.226C771.949 133.158 776.954 134.143 778.331 137.989L804.018 210.104C804.947 212.703 807.174 214.625 809.87 215.154L884.889 229.964C888.892 230.761 890.593 235.564 887.961 238.639L838.281 296.565C837.84 297.076 837.045 298.018 836.143 299.084C834.15 301.436 835.521 305.109 838.594 305.595C839.972 305.816 841.225 306.021 841.938 306.13L873.109 310.907C875.569 311.278 878.035 310.379 879.652 308.501L935.578 243.536C937.271 241.57 937.8 238.845 936.964 236.372L936.973 236.38Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M911.999 55.102L870.29 46.6989C866.886 46.0194 864.08 49.3142 865.318 52.5645L880.481 92.6507C881.489 95.3029 884.957 95.9449 886.793 93.8116L914.357 61.7933C916.398 59.4218 915.091 55.7117 912.006 55.0932L911.999 55.102Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M826.638 -18.5764L841.086 21.4922C842.259 24.7636 839.418 28.0277 836.025 27.2808L794.195 18.1733C791.425 17.5655 790.281 14.2265 792.117 12.0931L819.681 -19.9252C821.722 -22.2966 825.579 -21.5494 826.645 -18.5852L826.638 -18.5764Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M714.333 23.6796L725.854 58.1342C726.692 60.6248 728.806 62.4956 731.376 63.0081L812.205 79.1134C814.775 79.6261 816.895 81.4556 817.78 83.9254L845.62 161.601C846.505 164.07 848.657 165.898 851.248 166.351L886.981 172.699C889.555 173.154 892.161 172.237 893.853 170.271L949.78 105.306C951.389 103.437 951.921 100.853 951.192 98.4721L941.878 68.3045C941.687 67.6787 941.29 66.4447 940.846 65.0316C939.894 62.0436 936.04 61.237 934.031 63.6063C933.166 64.6113 932.377 65.5283 931.891 66.0925L881.997 123.834C879.356 126.902 874.352 125.917 872.975 122.07L847.287 49.9557C846.359 47.3559 844.132 45.4342 841.436 44.9053L766.417 30.0952C762.414 29.2988 760.713 24.4957 763.345 21.42L813.025 -36.5054C813.465 -37.0168 814.261 -37.959 815.163 -39.0246C817.156 -41.3761 815.785 -45.0493 812.712 -45.5353C811.333 -45.7567 810.081 -45.9618 809.368 -46.0702L778.196 -50.8475C775.737 -51.2183 773.271 -50.3197 771.654 -48.4419L715.727 16.5236C714.035 18.4896 713.506 21.2142 714.342 23.6873L714.333 23.6796Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M1164.7 -89.7906L1122.99 -98.1936C1119.59 -98.8731 1116.78 -95.5784 1118.02 -92.3281L1133.19 -52.2419C1134.19 -49.5896 1137.66 -48.9476 1139.5 -51.081L1167.06 -83.0993C1169.1 -85.4707 1167.79 -89.1809 1164.71 -89.7994L1164.7 -89.7906Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M1079.34 -163.469L1093.79 -123.4C1094.96 -120.129 1092.12 -116.865 1088.73 -117.612L1046.9 -126.719C1044.13 -127.327 1042.98 -130.666 1044.82 -132.799L1072.39 -164.818C1074.43 -167.189 1078.28 -166.442 1079.35 -163.478L1079.34 -163.469Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M967.037 -121.213L978.558 -86.7584C979.397 -84.2678 981.511 -82.397 984.081 -81.8845L1064.91 -65.7792C1067.48 -65.2665 1069.6 -63.437 1070.48 -60.9671L1098.32 16.7081C1099.21 19.1779 1101.36 21.0052 1103.95 21.4584L1139.69 27.8069C1142.26 28.2613 1144.87 27.3445 1146.56 25.3785L1202.48 -39.587C1204.09 -41.456 1204.63 -44.0395 1203.9 -46.4205L1194.58 -76.5881C1194.39 -77.2139 1193.99 -78.4479 1193.55 -79.861C1192.6 -82.849 1188.74 -83.6556 1186.74 -81.2863C1185.87 -80.2812 1185.08 -79.3643 1184.6 -78.8001L1134.7 -21.059C1132.06 -17.991 1127.06 -18.9759 1125.68 -22.8226L1099.99 -94.9369C1099.06 -97.5367 1096.84 -99.4584 1094.14 -99.9873L1019.12 -114.797C1015.12 -115.594 1013.42 -120.397 1016.05 -123.473L1065.73 -181.398C1066.17 -181.909 1066.97 -182.852 1067.87 -183.917C1069.86 -186.269 1068.49 -189.942 1065.42 -190.428C1064.04 -190.649 1062.79 -190.854 1062.07 -190.963L1030.9 -195.74C1028.44 -196.111 1025.97 -195.212 1024.36 -193.335L968.432 -128.369C966.739 -126.403 966.21 -123.678 967.046 -121.205L967.037 -121.213Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M972.308 204.959L1014.02 213.362C1017.42 214.042 1020.23 210.747 1018.99 207.497L1003.83 167.411C1002.82 164.758 999.35 164.116 997.514 166.25L969.95 198.268C967.908 200.639 969.216 204.35 972.3 204.968L972.308 204.959Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M1057.67 278.638L1043.22 238.569C1042.05 235.298 1044.89 232.034 1048.28 232.78L1090.11 241.888C1092.88 242.496 1094.03 245.835 1092.19 247.968L1064.63 279.987C1062.58 282.358 1058.73 281.611 1057.66 278.647L1057.67 278.638Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M1169.97 236.382L1158.45 201.927C1157.61 199.437 1155.5 197.566 1152.93 197.053L1072.1 180.948C1069.53 180.435 1067.41 178.606 1066.53 176.136L1038.69 98.4607C1037.8 95.9908 1035.65 94.1636 1033.06 93.7104L997.325 87.3618C994.751 86.9075 992.146 87.8242 990.453 89.7902L934.527 154.756C932.918 156.625 932.386 159.208 933.115 161.589L942.429 191.757C942.62 192.383 943.017 193.617 943.461 195.03C944.413 198.018 948.267 198.824 950.275 196.455C951.14 195.45 951.93 194.533 952.416 193.969L1002.31 136.228C1004.95 133.16 1009.95 134.145 1011.33 137.991L1037.02 210.106C1037.95 212.705 1040.17 214.627 1042.87 215.156L1117.89 229.966C1121.89 230.763 1123.59 235.566 1120.96 238.641L1071.28 296.567C1070.84 297.078 1070.05 298.02 1069.14 299.086C1067.15 301.437 1068.52 305.111 1071.6 305.597C1072.97 305.818 1074.23 306.023 1074.94 306.132L1106.11 310.909C1108.57 311.28 1111.04 310.381 1112.65 308.503L1168.58 243.538C1170.27 241.572 1170.8 238.847 1169.96 236.374L1169.97 236.382Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M1145 55.102L1103.29 46.6989C1099.89 46.0194 1097.08 49.3142 1098.32 52.5645L1113.48 92.6507C1114.49 95.3029 1117.96 95.9449 1119.79 93.8116L1147.36 61.7933C1149.4 59.4218 1148.09 55.7117 1145.01 55.0932L1145 55.102Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M1059.64 -18.5764L1074.09 21.4922C1075.26 24.7636 1072.42 28.0277 1069.03 27.2808L1027.2 18.1733C1024.43 17.5655 1023.28 14.2265 1025.12 12.0931L1052.68 -19.9252C1054.72 -22.2966 1058.58 -21.5494 1059.65 -18.5852L1059.64 -18.5764Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M947.334 23.6796L958.855 58.1342C959.694 60.6248 961.808 62.4956 964.377 63.0081L1045.21 79.1134C1047.78 79.6261 1049.9 81.4556 1050.78 83.9254L1078.62 161.601C1079.51 164.07 1081.66 165.898 1084.25 166.351L1119.98 172.699C1122.56 173.154 1125.16 172.237 1126.85 170.271L1182.78 105.306C1184.39 103.437 1184.92 100.853 1184.19 98.4721L1174.88 68.3045C1174.69 67.6787 1174.29 66.4447 1173.85 65.0316C1172.9 62.0436 1169.04 61.237 1167.03 63.6063C1166.17 64.6113 1165.38 65.5283 1164.89 66.0925L1115 123.834C1112.36 126.902 1107.35 125.917 1105.98 122.07L1080.29 49.9557C1079.36 47.3559 1077.13 45.4342 1074.44 44.9053L999.418 30.0952C995.415 29.2988 993.714 24.4957 996.346 21.42L1046.03 -36.5054C1046.47 -37.0168 1047.26 -37.959 1048.16 -39.0246C1050.16 -41.3761 1048.79 -45.0493 1045.71 -45.5353C1044.33 -45.7567 1043.08 -45.9618 1042.37 -46.0702L1011.2 -50.8475C1008.74 -51.2183 1006.27 -50.3197 1004.66 -48.4419L948.728 16.5236C947.036 18.4896 946.507 21.2142 947.343 23.6873L947.334 23.6796Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-10.2954 -89.7842L-52.0041 -98.1873C-55.4084 -98.8668 -58.2138 -95.572 -56.976 -92.3217L-41.8127 -52.2355C-40.805 -49.5833 -37.3374 -48.9413 -35.5009 -51.0746L-7.93733 -83.093C-5.89584 -85.4644 -7.20342 -89.1745 -10.2878 -89.7931L-10.2954 -89.7842Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-95.6563 -163.463L-81.2076 -123.394C-80.0346 -120.123 -82.8755 -116.859 -86.2686 -117.605L-128.099 -126.713C-130.869 -127.321 -132.013 -130.66 -130.177 -132.793L-102.613 -164.811C-100.572 -167.183 -96.7151 -166.436 -95.6487 -163.471L-95.6563 -163.463Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-207.961 -121.207L-196.44 -86.7521C-195.602 -84.2614 -193.488 -82.3906 -190.918 -81.8782L-110.089 -65.7728C-107.519 -65.2602 -105.399 -63.4306 -104.514 -60.9608L-76.6744 16.7144C-75.7891 19.1843 -73.6365 21.0115 -71.0462 21.4648L-35.3125 27.8132C-32.7387 28.2676 -30.1333 27.3509 -28.4408 25.3849L27.486 -39.5807C29.0949 -41.4496 29.627 -44.0332 28.898 -46.4142L19.5838 -76.5818C19.3929 -77.2076 18.996 -78.4415 18.5516 -79.8546C17.6002 -82.8427 13.746 -83.6492 11.7374 -81.2799C10.8722 -80.2749 10.0828 -79.358 9.59708 -78.7937L-40.2965 -21.0526C-42.9377 -17.9846 -47.9423 -18.9695 -49.3189 -22.8162L-75.0067 -94.9306C-75.9355 -97.5303 -78.1619 -99.452 -80.8578 -99.9809L-155.877 -114.791C-159.88 -115.587 -161.581 -120.391 -158.949 -123.466L-109.269 -181.392C-108.829 -181.903 -108.033 -182.845 -107.131 -183.911C-105.138 -186.262 -106.509 -189.935 -109.582 -190.422C-110.961 -190.643 -112.213 -190.848 -112.926 -190.956L-144.098 -195.734C-146.557 -196.105 -149.023 -195.206 -150.64 -193.328L-206.567 -128.363C-208.259 -126.397 -208.788 -123.672 -207.952 -121.199L-207.961 -121.207Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-202.692 204.964L-160.984 213.367C-157.579 214.046 -154.774 210.752 -156.012 207.501L-171.175 167.415C-172.183 164.763 -175.65 164.121 -177.487 166.254L-205.05 198.272C-207.092 200.644 -205.784 204.354 -202.7 204.973L-202.692 204.964Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-117.331 278.642L-131.78 238.574C-132.953 235.302 -130.112 232.038 -126.719 232.785L-84.8883 241.892C-82.119 242.5 -80.9743 245.839 -82.8109 247.973L-110.374 279.991C-112.416 282.362 -116.273 281.615 -117.339 278.651L-117.331 278.642Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-5.0264 236.386L-16.5478 201.932C-17.386 199.441 -19.5 197.57 -22.0699 197.058L-102.898 180.952C-105.469 180.44 -107.588 178.61 -108.474 176.14L-136.313 98.4651C-137.198 95.9952 -139.351 94.168 -141.941 93.7148L-177.675 87.3662C-180.249 86.9119 -182.854 87.8286 -184.547 89.7946L-240.474 154.76C-242.083 156.629 -242.615 159.213 -241.886 161.594L-232.571 191.761C-232.381 192.387 -231.984 193.621 -231.539 195.034C-230.588 198.022 -226.734 198.829 -224.725 196.459C-223.86 195.454 -223.07 194.537 -222.585 193.973L-172.691 136.232C-170.05 133.164 -165.045 134.149 -163.669 137.996L-137.981 210.11C-137.052 212.71 -134.826 214.631 -132.13 215.16L-57.1103 229.971C-53.1076 230.767 -51.4064 235.57 -54.0387 238.646L-103.719 296.571C-104.159 297.083 -104.955 298.025 -105.856 299.09C-107.85 301.442 -106.479 305.115 -103.405 305.601C-102.027 305.822 -100.775 306.028 -100.061 306.136L-68.8898 310.913C-66.4306 311.284 -63.9642 310.385 -62.3477 308.508L-6.42088 243.542C-4.72842 241.576 -4.19972 238.852 -5.03531 236.378L-5.0264 236.386Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-30.0004 55.1083L-71.7092 46.7053C-75.1135 46.0258 -77.9188 49.3206 -76.6811 52.5708L-61.5178 92.657C-60.5101 95.3093 -57.0425 95.9513 -55.206 93.8179L-27.6424 61.7996C-25.6009 59.4282 -26.9085 55.7181 -29.9929 55.0995L-30.0004 55.1083Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-115.361 -18.57L-100.913 21.4985C-99.7397 24.7699 -102.581 28.034 -105.974 27.2872L-147.804 18.1796C-150.574 17.5719 -151.718 14.2328 -149.882 12.0995L-122.318 -19.9189C-120.277 -22.2903 -116.42 -21.543 -115.354 -18.5788L-115.361 -18.57Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M-227.666 23.6859L-216.145 58.1405C-215.307 60.6312 -213.193 62.5019 -210.623 63.0144L-129.794 79.1198C-127.224 79.6324 -125.104 81.4619 -124.219 83.9318L-96.3795 161.607C-95.4942 164.077 -93.3416 165.904 -90.7513 166.357L-55.0176 172.706C-52.4438 173.16 -49.8384 172.243 -48.1459 170.277L7.78091 105.312C9.38986 103.443 9.9219 100.859 9.19295 98.4784L-0.121262 68.3108C-0.312151 67.685 -0.709083 66.451 -1.15346 65.038C-2.10493 62.0499 -5.95909 61.2434 -7.96773 63.6127C-8.83292 64.6177 -9.62228 65.5346 -10.108 66.0988L-60.0016 123.84C-62.6428 126.908 -67.6474 125.923 -69.0239 122.076L-94.7117 49.962C-95.6405 47.3623 -97.8669 45.4406 -100.563 44.9116L-175.582 30.1016C-179.585 29.3052 -181.286 24.5021 -178.654 21.4264L-128.974 -36.4991C-128.534 -37.0104 -127.738 -37.9527 -126.836 -39.0182C-124.843 -41.3698 -126.214 -45.0429 -129.288 -45.5289C-130.666 -45.7503 -131.918 -45.9554 -132.631 -46.0639L-163.803 -50.8412C-166.262 -51.212 -168.728 -50.3134 -170.345 -48.4356L-226.272 16.5299C-227.964 18.4959 -228.493 21.2206 -227.657 23.6936L-227.666 23.6859Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M222.706 -89.7842L180.997 -98.1873C177.593 -98.8668 174.787 -95.572 176.025 -92.3217L191.188 -52.2355C192.196 -49.5833 195.663 -48.9413 197.5 -51.0746L225.064 -83.093C227.105 -85.4644 225.798 -89.1745 222.713 -89.7931L222.706 -89.7842Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M137.345 -163.463L151.793 -123.394C152.966 -120.123 150.125 -116.859 146.732 -117.605L104.902 -126.713C102.132 -127.321 100.988 -130.66 102.824 -132.793L130.388 -164.811C132.429 -167.183 136.286 -166.436 137.352 -163.471L137.345 -163.463Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M25.0397 -121.207L36.5611 -86.7521C37.3993 -84.2614 39.5133 -82.3906 42.0832 -81.8782L122.912 -65.7728C125.482 -65.2602 127.602 -63.4306 128.487 -60.9608L156.327 16.7144C157.212 19.1843 159.364 21.0115 161.955 21.4648L197.688 27.8132C200.262 28.2676 202.868 27.3509 204.56 25.3849L260.487 -39.5807C262.096 -41.4496 262.628 -44.0332 261.899 -46.4142L252.585 -76.5818C252.394 -77.2076 251.997 -78.4415 251.553 -79.8546C250.601 -82.8427 246.747 -83.6492 244.738 -81.2799C243.873 -80.2749 243.084 -79.358 242.598 -78.7937L192.704 -21.0526C190.063 -17.9846 185.059 -18.9695 183.682 -22.8162L157.994 -94.9306C157.066 -97.5303 154.839 -99.452 152.143 -99.9809L77.1236 -114.791C73.1209 -115.587 71.4197 -120.391 74.052 -123.466L123.732 -181.392C124.172 -181.903 124.968 -182.845 125.87 -183.911C127.863 -186.262 126.492 -189.935 123.418 -190.422C122.04 -190.643 120.788 -190.848 120.075 -190.956L88.9031 -195.734C86.4439 -196.105 83.9776 -195.206 82.361 -193.328L26.4342 -128.363C24.7417 -126.397 24.213 -123.672 25.0486 -121.199L25.0397 -121.207Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M30.3106 204.966L72.0193 213.369C75.4237 214.048 78.229 210.753 76.9913 207.503L61.828 167.417C60.8203 164.765 57.3527 164.123 55.5162 166.256L27.9526 198.274C25.9111 200.646 27.2187 204.356 30.303 204.975L30.3106 204.966Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M115.672 278.644L101.223 238.576C100.05 235.304 102.891 232.04 106.284 232.787L148.115 241.894C150.884 242.502 152.029 245.841 150.192 247.975L122.628 279.993C120.587 282.364 116.73 281.617 115.664 278.653L115.672 278.644Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M227.977 236.388L216.455 201.934C215.617 199.443 213.503 197.572 210.933 197.06L130.104 180.954C127.534 180.442 125.415 178.612 124.529 176.142L96.6897 98.467C95.8044 95.9972 93.6518 94.17 91.0615 93.7167L55.3278 87.3682C52.754 86.9138 50.1486 87.8306 48.4561 89.7966L-7.47073 154.762C-9.07968 156.631 -9.61172 159.215 -8.88277 161.596L0.431442 191.763C0.622332 192.389 1.01926 193.623 1.46364 195.036C2.41511 198.024 6.26927 198.831 8.27791 196.461C9.1431 195.456 9.93247 194.539 10.4182 193.975L60.3118 136.234C62.953 133.166 67.9575 134.151 69.3341 137.998L95.0219 210.112C95.9507 212.712 98.1771 214.633 100.873 215.162L175.893 229.972C179.895 230.769 181.597 235.572 178.964 238.648L129.284 296.573C128.844 297.084 128.048 298.027 127.147 299.092C125.153 301.444 126.524 305.117 129.598 305.603C130.976 305.824 132.228 306.029 132.942 306.138L164.113 310.915C166.572 311.286 169.039 310.387 170.655 308.51L226.582 243.544C228.274 241.578 228.803 238.853 227.968 236.38L227.977 236.388Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M203.002 55.1083L161.294 46.7053C157.889 46.0258 155.084 49.3206 156.322 52.5708L171.485 92.657C172.493 95.3093 175.96 95.9513 177.797 93.8179L205.361 61.7996C207.402 59.4282 206.094 55.7181 203.01 55.0995L203.002 55.1083Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M117.642 -18.57L132.09 21.4985C133.263 24.7699 130.422 28.034 127.029 27.2872L85.1985 18.1796C82.4292 17.5719 81.2845 14.2328 83.121 12.0995L110.685 -19.9189C112.726 -22.2903 116.583 -21.543 117.649 -18.5788L117.642 -18.57Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M5.33659 23.6859L16.858 58.1405C17.6961 60.6312 19.8102 62.5019 22.3801 63.0144L103.209 79.1198C105.779 79.6324 107.898 81.4619 108.784 83.9318L136.623 161.607C137.509 164.077 139.661 165.904 142.252 166.357L177.985 172.706C180.559 173.16 183.165 172.243 184.857 170.277L240.784 105.312C242.393 103.443 242.925 100.859 242.196 98.4784L232.882 68.3108C232.691 67.685 232.294 66.451 231.849 65.038C230.898 62.0499 227.044 61.2434 225.035 63.6127C224.17 64.6177 223.381 65.5346 222.895 66.0988L173.001 123.84C170.36 126.908 165.356 125.923 163.979 122.076L138.291 49.962C137.362 47.3623 135.136 45.4406 132.44 44.9116L57.4205 30.1016C53.4178 29.3052 51.7166 24.5021 54.3488 21.4264L104.029 -36.4991C104.469 -37.0104 105.265 -37.9527 106.167 -39.0182C108.16 -41.3698 106.789 -45.0429 103.715 -45.5289C102.337 -45.7503 101.085 -45.9554 100.372 -46.0639L69.2 -50.8412C66.7408 -51.212 64.2744 -50.3134 62.6579 -48.4356L6.73106 16.5299C5.0386 18.4959 4.5099 21.2206 5.34549 23.6936L5.33659 23.6859Z"
|
||||
fill="currentColor" fill-opacity="0.08" />
|
||||
<path
|
||||
d="M460.704 -89.7906L418.995 -98.1936C415.591 -98.8731 412.786 -95.5784 414.023 -92.3281L429.187 -52.2419C430.194 -49.5896 433.662 -48.9476 435.498 -51.081L463.062 -83.0993C465.103 -85.4707 463.796 -89.1809 460.711 -89.7994L460.704 -89.7906Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M375.343 -163.469L389.792 -123.4C390.965 -120.129 388.124 -116.865 384.731 -117.612L342.9 -126.719C340.131 -127.327 338.986 -130.666 340.823 -132.799L368.386 -164.818C370.428 -167.189 374.284 -166.442 375.351 -163.478L375.343 -163.469Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M263.038 -121.213L274.559 -86.7584C275.398 -84.2678 277.512 -82.397 280.082 -81.8845L360.91 -65.7792C363.48 -65.2665 365.6 -63.437 366.485 -60.9671L394.325 16.7081C395.21 19.1779 397.363 21.0052 399.953 21.4584L435.687 27.8069C438.261 28.2613 440.866 27.3445 442.558 25.3785L498.485 -39.587C500.094 -41.456 500.626 -44.0395 499.897 -46.4205L490.583 -76.5881C490.392 -77.2139 489.995 -78.4479 489.551 -79.861C488.599 -82.849 484.745 -83.6556 482.737 -81.2863C481.871 -80.2812 481.082 -79.3643 480.596 -78.8001L430.703 -21.059C428.062 -17.991 423.057 -18.9759 421.68 -22.8226L395.993 -94.9369C395.064 -97.5367 392.837 -99.4584 390.141 -99.9873L315.122 -114.797C311.119 -115.594 309.418 -120.397 312.05 -123.473L361.73 -181.398C362.171 -181.909 362.966 -182.852 363.868 -183.917C365.861 -186.269 364.49 -189.942 361.417 -190.428C360.039 -190.649 358.786 -190.854 358.073 -190.963L326.901 -195.74C324.442 -196.111 321.976 -195.212 320.359 -193.335L264.433 -128.369C262.74 -126.403 262.211 -123.678 263.047 -121.205L263.038 -121.213Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M268.307 204.958L310.016 213.361C313.42 214.041 316.226 210.746 314.988 207.496L299.825 167.41C298.817 164.757 295.349 164.115 293.513 166.249L265.949 198.267C263.908 200.639 265.215 204.349 268.3 204.967L268.307 204.958Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M353.668 278.637L339.219 238.568C338.046 235.297 340.887 232.033 344.28 232.78L386.111 241.887C388.88 242.495 390.025 245.834 388.189 247.967L360.625 279.986C358.584 282.357 354.727 281.61 353.66 278.646L353.668 278.637Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M465.973 236.381L454.452 201.926C453.613 199.436 451.499 197.565 448.93 197.052L368.101 180.947C365.531 180.434 363.411 178.605 362.526 176.135L334.686 98.4597C333.801 95.9899 331.648 94.1626 329.058 93.7094L293.324 87.3609C290.75 86.9065 288.145 87.8232 286.453 89.7892L230.526 154.755C228.917 156.624 228.385 159.207 229.114 161.588L238.428 191.756C238.619 192.382 239.016 193.616 239.46 195.029C240.412 198.017 244.266 198.823 246.274 196.454C247.14 195.449 247.929 194.532 248.415 193.968L298.308 136.227C300.95 133.159 305.954 134.144 307.331 137.99L333.018 210.105C333.947 212.704 336.174 214.626 338.87 215.155L413.889 229.965C417.892 230.762 419.593 235.565 416.961 238.64L367.281 296.566C366.84 297.077 366.045 298.019 365.143 299.085C363.15 301.436 364.521 305.11 367.594 305.596C368.972 305.817 370.225 306.022 370.938 306.131L402.11 310.908C404.569 311.279 407.035 310.38 408.652 308.502L464.579 243.537C466.271 241.571 466.8 238.846 465.964 236.373L465.973 236.381Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M440.999 55.1022L399.29 46.6992C395.886 46.0197 393.08 49.3145 394.318 52.5647L409.482 92.6509C410.489 95.3032 413.957 95.9452 415.793 93.8118L443.357 61.7935C445.398 59.4221 444.091 55.7119 441.006 55.0934L440.999 55.1022Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M355.638 -18.5761L370.087 21.4924C371.26 24.7638 368.419 28.0279 365.026 27.2811L323.195 18.1735C320.426 17.5658 319.281 14.2267 321.117 12.0934L348.681 -19.925C350.723 -22.2964 354.579 -21.5491 355.646 -18.5849L355.638 -18.5761Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M243.333 23.6798L254.854 58.1344C255.693 60.6251 257.807 62.4958 260.376 63.0083L341.205 79.1136C343.775 79.6263 345.895 81.4558 346.78 83.9257L374.62 161.601C375.505 164.071 377.658 165.898 380.248 166.351L415.982 172.7C418.555 173.154 421.161 172.237 422.853 170.271L478.78 105.306C480.389 103.437 480.921 100.853 480.192 98.4723L470.878 68.3047C470.687 67.6789 470.29 66.4449 469.846 65.0319C468.894 62.0438 465.04 61.2373 463.032 63.6066C462.166 64.6116 461.377 65.5285 460.891 66.0927L410.998 123.834C408.357 126.902 403.352 125.917 401.975 122.07L376.288 49.9559C375.359 47.3562 373.132 45.4345 370.436 44.9055L295.417 30.0955C291.414 29.2991 289.713 24.496 292.345 21.4203L342.025 -36.5052C342.466 -37.0165 343.261 -37.9588 344.163 -39.0243C346.156 -41.3759 344.785 -45.049 341.712 -45.535C340.334 -45.7564 339.081 -45.9615 338.368 -46.07L307.196 -50.8473C304.737 -51.2181 302.271 -50.3195 300.654 -48.4417L244.727 16.5238C243.035 18.4898 242.506 21.2145 243.342 23.6875L243.333 23.6798Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M693.703 -89.7894L651.995 -98.1924C648.59 -98.8719 645.785 -95.5771 647.023 -92.3269L662.186 -52.2407C663.194 -49.5884 666.661 -48.9464 668.498 -51.0798L696.061 -83.0981C698.103 -85.4695 696.795 -89.1797 693.711 -89.7982L693.703 -89.7894Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M608.342 -163.468L622.791 -123.399C623.964 -120.128 621.123 -116.864 617.73 -117.611L575.899 -126.718C573.13 -127.326 571.985 -130.665 573.822 -132.798L601.385 -164.817C603.427 -167.188 607.283 -166.441 608.35 -163.477L608.342 -163.468Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M496.037 -121.212L507.559 -86.7572C508.397 -84.2665 510.511 -82.3958 513.081 -81.8833L593.909 -65.778C596.48 -65.2653 598.599 -63.4358 599.484 -60.9659L627.324 16.7093C628.209 19.1791 630.362 21.0064 632.952 21.4596L668.686 27.8081C671.26 28.2625 673.865 27.3458 675.558 25.3798L731.485 -39.5858C733.094 -41.4548 733.626 -44.0383 732.897 -46.4193L723.582 -76.5869C723.392 -77.2127 722.995 -78.4467 722.55 -79.8598C721.599 -82.8478 717.745 -83.6543 715.736 -81.285C714.871 -80.28 714.081 -79.3631 713.596 -78.7989L663.702 -21.0578C661.061 -17.9897 656.056 -18.9746 654.68 -22.8213L628.992 -94.9357C628.063 -97.5354 625.837 -99.4571 623.141 -99.9861L548.121 -114.796C544.118 -115.593 542.417 -120.396 545.05 -123.471L594.73 -181.397C595.17 -181.908 595.966 -182.85 596.867 -183.916C598.861 -186.267 597.49 -189.941 594.416 -190.427C593.038 -190.648 591.785 -190.853 591.072 -190.962L559.901 -195.739C557.441 -196.11 554.975 -195.211 553.359 -193.333L497.432 -128.368C495.739 -126.402 495.211 -123.677 496.046 -121.204L496.037 -121.212Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M501.308 204.961L543.017 213.364C546.421 214.043 549.227 210.748 547.989 207.498L532.826 167.412C531.818 164.76 528.35 164.118 526.514 166.251L498.95 198.269C496.909 200.641 498.216 204.351 501.301 204.969L501.308 204.961Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M586.669 278.639L572.221 238.57C571.047 235.299 573.888 232.035 577.282 232.782L619.112 241.889C621.882 242.497 623.026 245.836 621.19 247.969L593.626 279.988C591.585 282.359 587.728 281.612 586.662 278.648L586.669 278.639Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M698.974 236.383L687.453 201.928C686.615 199.438 684.501 197.567 681.931 197.054L601.102 180.949C598.532 180.436 596.412 178.607 595.527 176.137L567.687 98.4619C566.802 95.9921 564.649 94.1648 562.059 93.7116L526.325 87.3631C523.752 86.9087 521.146 87.8254 519.454 89.7914L463.527 154.757C461.918 156.626 461.386 159.209 462.115 161.59L471.429 191.758C471.62 192.384 472.017 193.618 472.461 195.031C473.413 198.019 477.267 198.826 479.276 196.456C480.141 195.451 480.93 194.534 481.416 193.97L531.309 136.229C533.951 133.161 538.955 134.146 540.332 137.993L566.02 210.107C566.948 212.707 569.175 214.628 571.871 215.157L646.89 229.967C650.893 230.764 652.594 235.567 649.962 238.643L600.282 296.568C599.842 297.079 599.046 298.022 598.144 299.087C596.151 301.439 597.522 305.112 600.595 305.598C601.973 305.819 603.226 306.024 603.939 306.133L635.111 310.91C637.57 311.281 640.036 310.382 641.653 308.504L697.58 243.539C699.272 241.573 699.801 238.848 698.965 236.375L698.974 236.383Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M674 55.1032L632.291 46.7001C628.887 46.0206 626.082 49.3155 627.319 52.5657L642.483 92.6519C643.49 95.3041 646.958 95.9462 648.795 93.8128L676.358 61.7945C678.4 59.4231 677.092 55.7129 674.008 55.0944L674 55.1032Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M588.639 -18.5751L603.088 21.4934C604.261 24.7648 601.42 28.0289 598.027 27.2821L556.196 18.1745C553.427 17.5667 552.282 14.2277 554.119 12.0943L581.682 -19.924C583.724 -22.2954 587.58 -21.5481 588.647 -18.584L588.639 -18.5751Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M476.334 23.6808L487.856 58.1354C488.694 60.626 490.808 62.4968 493.378 63.0093L574.206 79.1146C576.776 79.6273 578.896 81.4568 579.781 83.9267L607.621 161.602C608.506 164.072 610.659 165.899 613.249 166.352L648.983 172.701C651.557 173.155 654.162 172.238 655.855 170.272L711.781 105.307C713.39 103.438 713.922 100.854 713.193 98.4733L703.879 68.3057C703.688 67.6799 703.291 66.4459 702.847 65.0328C701.896 62.0448 698.041 61.2382 696.033 63.6075C695.168 64.6126 694.378 65.5295 693.893 66.0937L643.999 123.835C641.358 126.903 636.353 125.918 634.977 122.071L609.289 49.9569C608.36 47.3571 606.134 45.4354 603.438 44.9065L528.418 30.0964C524.415 29.3 522.714 24.4969 525.346 21.4212L575.027 -36.5042C575.467 -37.0156 576.262 -37.9578 577.164 -39.0234C579.158 -41.3749 577.786 -45.048 574.713 -45.5341C573.335 -45.7554 572.082 -45.9606 571.369 -46.069L540.198 -50.8463C537.738 -51.2171 535.272 -50.3185 533.656 -48.4407L477.729 16.5248C476.036 18.4908 475.508 21.2154 476.343 23.6885L476.334 23.6808Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M931.704 -89.7906L889.995 -98.1936C886.591 -98.8731 883.785 -95.5784 885.023 -92.3281L900.186 -52.2419C901.194 -49.5896 904.662 -48.9476 906.498 -51.081L934.062 -83.0993C936.103 -85.4707 934.796 -89.1809 931.711 -89.7994L931.704 -89.7906Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M846.343 -163.469L860.791 -123.4C861.964 -120.129 859.124 -116.865 855.73 -117.612L813.9 -126.719C811.13 -127.327 809.986 -130.666 811.822 -132.799L839.386 -164.818C841.427 -167.189 845.284 -166.442 846.35 -163.478L846.343 -163.469Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M734.038 -121.213L745.559 -86.7584C746.397 -84.2678 748.511 -82.397 751.081 -81.8845L831.91 -65.7792C834.48 -65.2665 836.6 -63.437 837.485 -60.9671L865.325 16.7081C866.21 19.1779 868.362 21.0052 870.953 21.4584L906.687 27.8069C909.26 28.2613 911.866 27.3445 913.558 25.3785L969.485 -39.587C971.094 -41.456 971.626 -44.0395 970.897 -46.4205L961.583 -76.5881C961.392 -77.2139 960.995 -78.4479 960.551 -79.861C959.599 -82.849 955.745 -83.6556 953.736 -81.2863C952.871 -80.2812 952.082 -79.3643 951.596 -78.8001L901.703 -21.059C899.061 -17.991 894.057 -18.9759 892.68 -22.8226L866.992 -94.9369C866.064 -97.5367 863.837 -99.4584 861.141 -99.9873L786.122 -114.797C782.119 -115.594 780.418 -120.397 783.05 -123.473L832.73 -181.398C833.17 -181.909 833.966 -182.852 834.868 -183.917C836.861 -186.269 835.49 -189.942 832.417 -190.428C831.039 -190.649 829.786 -190.854 829.073 -190.963L797.901 -195.74C795.442 -196.111 792.976 -195.212 791.359 -193.335L735.432 -128.369C733.74 -126.403 733.211 -123.678 734.047 -121.205L734.038 -121.213Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M739.307 204.957L781.016 213.36C784.42 214.04 787.225 210.745 785.988 207.495L770.824 167.409C769.817 164.756 766.349 164.114 764.513 166.248L736.949 198.266C734.907 200.638 736.215 204.348 739.299 204.966L739.307 204.957Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M824.668 278.636L810.219 238.567C809.046 235.296 811.887 232.032 815.28 232.779L857.111 241.886C859.88 242.494 861.025 245.833 859.188 247.966L831.625 279.985C829.583 282.356 825.727 281.609 824.66 278.645L824.668 278.636Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M936.973 236.38L925.452 201.925C924.613 199.435 922.499 197.564 919.929 197.051L839.101 180.946C836.531 180.433 834.411 178.604 833.526 176.134L805.686 98.4587C804.801 95.9889 802.648 94.1616 800.058 93.7084L764.324 87.3599C761.75 86.9055 759.145 87.8223 757.452 89.7883L701.526 154.754C699.917 156.623 699.385 159.206 700.114 161.587L709.428 191.755C709.619 192.381 710.016 193.615 710.46 195.028C711.411 198.016 715.266 198.822 717.274 196.453C718.139 195.448 718.929 194.531 719.415 193.967L769.308 136.226C771.949 133.158 776.954 134.143 778.331 137.989L804.018 210.104C804.947 212.703 807.174 214.625 809.87 215.154L884.889 229.964C888.892 230.761 890.593 235.564 887.961 238.639L838.281 296.565C837.84 297.076 837.045 298.018 836.143 299.084C834.15 301.436 835.521 305.109 838.594 305.595C839.972 305.816 841.225 306.021 841.938 306.13L873.109 310.907C875.569 311.278 878.035 310.379 879.652 308.501L935.578 243.536C937.271 241.57 937.8 238.845 936.964 236.372L936.973 236.38Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M911.999 55.102L870.29 46.6989C866.886 46.0194 864.08 49.3142 865.318 52.5645L880.481 92.6507C881.489 95.3029 884.957 95.9449 886.793 93.8116L914.357 61.7933C916.398 59.4218 915.091 55.7117 912.006 55.0932L911.999 55.102Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M826.638 -18.5764L841.086 21.4922C842.259 24.7636 839.418 28.0277 836.025 27.2808L794.195 18.1733C791.425 17.5655 790.281 14.2265 792.117 12.0931L819.681 -19.9252C821.722 -22.2966 825.579 -21.5494 826.645 -18.5852L826.638 -18.5764Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M714.333 23.6796L725.854 58.1342C726.692 60.6248 728.806 62.4956 731.376 63.0081L812.205 79.1134C814.775 79.6261 816.895 81.4556 817.78 83.9254L845.62 161.601C846.505 164.07 848.657 165.898 851.248 166.351L886.981 172.699C889.555 173.154 892.161 172.237 893.853 170.271L949.78 105.306C951.389 103.437 951.921 100.853 951.192 98.4721L941.878 68.3045C941.687 67.6787 941.29 66.4447 940.846 65.0316C939.894 62.0436 936.04 61.237 934.031 63.6063C933.166 64.6113 932.377 65.5283 931.891 66.0925L881.997 123.834C879.356 126.902 874.352 125.917 872.975 122.07L847.287 49.9557C846.359 47.3559 844.132 45.4342 841.436 44.9053L766.417 30.0952C762.414 29.2988 760.713 24.4957 763.345 21.42L813.025 -36.5054C813.465 -37.0168 814.261 -37.959 815.163 -39.0246C817.156 -41.3761 815.785 -45.0493 812.712 -45.5353C811.333 -45.7567 810.081 -45.9618 809.368 -46.0702L778.196 -50.8475C775.737 -51.2183 773.271 -50.3197 771.654 -48.4419L715.727 16.5236C714.035 18.4896 713.506 21.2142 714.342 23.6873L714.333 23.6796Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M1164.7 -89.7906L1122.99 -98.1936C1119.59 -98.8731 1116.78 -95.5784 1118.02 -92.3281L1133.19 -52.2419C1134.19 -49.5896 1137.66 -48.9476 1139.5 -51.081L1167.06 -83.0993C1169.1 -85.4707 1167.79 -89.1809 1164.71 -89.7994L1164.7 -89.7906Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M1079.34 -163.469L1093.79 -123.4C1094.96 -120.129 1092.12 -116.865 1088.73 -117.612L1046.9 -126.719C1044.13 -127.327 1042.98 -130.666 1044.82 -132.799L1072.39 -164.818C1074.43 -167.189 1078.28 -166.442 1079.35 -163.478L1079.34 -163.469Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M967.037 -121.213L978.558 -86.7584C979.397 -84.2678 981.511 -82.397 984.081 -81.8845L1064.91 -65.7792C1067.48 -65.2665 1069.6 -63.437 1070.48 -60.9671L1098.32 16.7081C1099.21 19.1779 1101.36 21.0052 1103.95 21.4584L1139.69 27.8069C1142.26 28.2613 1144.87 27.3445 1146.56 25.3785L1202.48 -39.587C1204.09 -41.456 1204.63 -44.0395 1203.9 -46.4205L1194.58 -76.5881C1194.39 -77.2139 1193.99 -78.4479 1193.55 -79.861C1192.6 -82.849 1188.74 -83.6556 1186.74 -81.2863C1185.87 -80.2812 1185.08 -79.3643 1184.6 -78.8001L1134.7 -21.059C1132.06 -17.991 1127.06 -18.9759 1125.68 -22.8226L1099.99 -94.9369C1099.06 -97.5367 1096.84 -99.4584 1094.14 -99.9873L1019.12 -114.797C1015.12 -115.594 1013.42 -120.397 1016.05 -123.473L1065.73 -181.398C1066.17 -181.909 1066.97 -182.852 1067.87 -183.917C1069.86 -186.269 1068.49 -189.942 1065.42 -190.428C1064.04 -190.649 1062.79 -190.854 1062.07 -190.963L1030.9 -195.74C1028.44 -196.111 1025.97 -195.212 1024.36 -193.335L968.432 -128.369C966.739 -126.403 966.21 -123.678 967.046 -121.205L967.037 -121.213Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M972.308 204.959L1014.02 213.362C1017.42 214.042 1020.23 210.747 1018.99 207.497L1003.83 167.411C1002.82 164.758 999.35 164.116 997.514 166.25L969.95 198.268C967.908 200.639 969.216 204.35 972.3 204.968L972.308 204.959Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M1057.67 278.638L1043.22 238.569C1042.05 235.298 1044.89 232.034 1048.28 232.78L1090.11 241.888C1092.88 242.496 1094.03 245.835 1092.19 247.968L1064.63 279.987C1062.58 282.358 1058.73 281.611 1057.66 278.647L1057.67 278.638Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M1169.97 236.382L1158.45 201.927C1157.61 199.437 1155.5 197.566 1152.93 197.053L1072.1 180.948C1069.53 180.435 1067.41 178.606 1066.53 176.136L1038.69 98.4607C1037.8 95.9908 1035.65 94.1636 1033.06 93.7104L997.325 87.3618C994.751 86.9075 992.146 87.8242 990.453 89.7902L934.527 154.756C932.918 156.625 932.386 159.208 933.115 161.589L942.429 191.757C942.62 192.383 943.017 193.617 943.461 195.03C944.413 198.018 948.267 198.824 950.275 196.455C951.14 195.45 951.93 194.533 952.416 193.969L1002.31 136.228C1004.95 133.16 1009.95 134.145 1011.33 137.991L1037.02 210.106C1037.95 212.705 1040.17 214.627 1042.87 215.156L1117.89 229.966C1121.89 230.763 1123.59 235.566 1120.96 238.641L1071.28 296.567C1070.84 297.078 1070.05 298.02 1069.14 299.086C1067.15 301.437 1068.52 305.111 1071.6 305.597C1072.97 305.818 1074.23 306.023 1074.94 306.132L1106.11 310.909C1108.57 311.28 1111.04 310.381 1112.65 308.503L1168.58 243.538C1170.27 241.572 1170.8 238.847 1169.96 236.374L1169.97 236.382Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M1145 55.102L1103.29 46.6989C1099.89 46.0194 1097.08 49.3142 1098.32 52.5645L1113.48 92.6507C1114.49 95.3029 1117.96 95.9449 1119.79 93.8116L1147.36 61.7933C1149.4 59.4218 1148.09 55.7117 1145.01 55.0932L1145 55.102Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M1059.64 -18.5764L1074.09 21.4922C1075.26 24.7636 1072.42 28.0277 1069.03 27.2808L1027.2 18.1733C1024.43 17.5655 1023.28 14.2265 1025.12 12.0931L1052.68 -19.9252C1054.72 -22.2966 1058.58 -21.5494 1059.65 -18.5852L1059.64 -18.5764Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M947.334 23.6796L958.855 58.1342C959.694 60.6248 961.808 62.4956 964.377 63.0081L1045.21 79.1134C1047.78 79.6261 1049.9 81.4556 1050.78 83.9254L1078.62 161.601C1079.51 164.07 1081.66 165.898 1084.25 166.351L1119.98 172.699C1122.56 173.154 1125.16 172.237 1126.85 170.271L1182.78 105.306C1184.39 103.437 1184.92 100.853 1184.19 98.4721L1174.88 68.3045C1174.69 67.6787 1174.29 66.4447 1173.85 65.0316C1172.9 62.0436 1169.04 61.237 1167.03 63.6063C1166.17 64.6113 1165.38 65.5283 1164.89 66.0925L1115 123.834C1112.36 126.902 1107.35 125.917 1105.98 122.07L1080.29 49.9557C1079.36 47.3559 1077.13 45.4342 1074.44 44.9053L999.418 30.0952C995.415 29.2988 993.714 24.4957 996.346 21.42L1046.03 -36.5054C1046.47 -37.0168 1047.26 -37.959 1048.16 -39.0246C1050.16 -41.3761 1048.79 -45.0493 1045.71 -45.5353C1044.33 -45.7567 1043.08 -45.9618 1042.37 -46.0702L1011.2 -50.8475C1008.74 -51.2183 1006.27 -50.3197 1004.66 -48.4419L948.728 16.5236C947.036 18.4896 946.507 21.2142 947.343 23.6873L947.334 23.6796Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-10.2954 -89.7842L-52.0041 -98.1873C-55.4084 -98.8668 -58.2138 -95.572 -56.976 -92.3217L-41.8127 -52.2355C-40.805 -49.5833 -37.3374 -48.9413 -35.5009 -51.0746L-7.93733 -83.093C-5.89584 -85.4644 -7.20342 -89.1745 -10.2878 -89.7931L-10.2954 -89.7842Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-95.6563 -163.463L-81.2076 -123.394C-80.0346 -120.123 -82.8755 -116.859 -86.2686 -117.605L-128.099 -126.713C-130.869 -127.321 -132.013 -130.66 -130.177 -132.793L-102.613 -164.811C-100.572 -167.183 -96.7151 -166.436 -95.6487 -163.471L-95.6563 -163.463Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-207.961 -121.207L-196.44 -86.7521C-195.602 -84.2614 -193.488 -82.3906 -190.918 -81.8782L-110.089 -65.7728C-107.519 -65.2602 -105.399 -63.4306 -104.514 -60.9608L-76.6744 16.7144C-75.7891 19.1843 -73.6365 21.0115 -71.0462 21.4648L-35.3125 27.8132C-32.7387 28.2676 -30.1333 27.3509 -28.4408 25.3849L27.486 -39.5807C29.0949 -41.4496 29.627 -44.0332 28.898 -46.4142L19.5838 -76.5818C19.3929 -77.2076 18.996 -78.4415 18.5516 -79.8546C17.6002 -82.8427 13.746 -83.6492 11.7374 -81.2799C10.8722 -80.2749 10.0828 -79.358 9.59708 -78.7937L-40.2965 -21.0526C-42.9377 -17.9846 -47.9423 -18.9695 -49.3189 -22.8162L-75.0067 -94.9306C-75.9355 -97.5303 -78.1619 -99.452 -80.8578 -99.9809L-155.877 -114.791C-159.88 -115.587 -161.581 -120.391 -158.949 -123.466L-109.269 -181.392C-108.829 -181.903 -108.033 -182.845 -107.131 -183.911C-105.138 -186.262 -106.509 -189.935 -109.582 -190.422C-110.961 -190.643 -112.213 -190.848 -112.926 -190.956L-144.098 -195.734C-146.557 -196.105 -149.023 -195.206 -150.64 -193.328L-206.567 -128.363C-208.259 -126.397 -208.788 -123.672 -207.952 -121.199L-207.961 -121.207Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-202.692 204.964L-160.984 213.367C-157.579 214.046 -154.774 210.752 -156.012 207.501L-171.175 167.415C-172.183 164.763 -175.65 164.121 -177.487 166.254L-205.05 198.272C-207.092 200.644 -205.784 204.354 -202.7 204.973L-202.692 204.964Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-117.331 278.642L-131.78 238.574C-132.953 235.302 -130.112 232.038 -126.719 232.785L-84.8883 241.892C-82.119 242.5 -80.9743 245.839 -82.8109 247.973L-110.374 279.991C-112.416 282.362 -116.273 281.615 -117.339 278.651L-117.331 278.642Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-5.0264 236.386L-16.5478 201.932C-17.386 199.441 -19.5 197.57 -22.0699 197.058L-102.898 180.952C-105.469 180.44 -107.588 178.61 -108.474 176.14L-136.313 98.4651C-137.198 95.9952 -139.351 94.168 -141.941 93.7148L-177.675 87.3662C-180.249 86.9119 -182.854 87.8286 -184.547 89.7946L-240.474 154.76C-242.083 156.629 -242.615 159.213 -241.886 161.594L-232.571 191.761C-232.381 192.387 -231.984 193.621 -231.539 195.034C-230.588 198.022 -226.734 198.829 -224.725 196.459C-223.86 195.454 -223.07 194.537 -222.585 193.973L-172.691 136.232C-170.05 133.164 -165.045 134.149 -163.669 137.996L-137.981 210.11C-137.052 212.71 -134.826 214.631 -132.13 215.16L-57.1103 229.971C-53.1076 230.767 -51.4064 235.57 -54.0387 238.646L-103.719 296.571C-104.159 297.083 -104.955 298.025 -105.856 299.09C-107.85 301.442 -106.479 305.115 -103.405 305.601C-102.027 305.822 -100.775 306.028 -100.061 306.136L-68.8898 310.913C-66.4306 311.284 -63.9642 310.385 -62.3477 308.508L-6.42088 243.542C-4.72842 241.576 -4.19972 238.852 -5.03531 236.378L-5.0264 236.386Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-30.0004 55.1083L-71.7092 46.7053C-75.1135 46.0258 -77.9188 49.3206 -76.6811 52.5708L-61.5178 92.657C-60.5101 95.3093 -57.0425 95.9513 -55.206 93.8179L-27.6424 61.7996C-25.6009 59.4282 -26.9085 55.7181 -29.9929 55.0995L-30.0004 55.1083Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-115.361 -18.57L-100.913 21.4985C-99.7397 24.7699 -102.581 28.034 -105.974 27.2872L-147.804 18.1796C-150.574 17.5719 -151.718 14.2328 -149.882 12.0995L-122.318 -19.9189C-120.277 -22.2903 -116.42 -21.543 -115.354 -18.5788L-115.361 -18.57Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M-227.666 23.6859L-216.145 58.1405C-215.307 60.6312 -213.193 62.5019 -210.623 63.0144L-129.794 79.1198C-127.224 79.6324 -125.104 81.4619 -124.219 83.9318L-96.3795 161.607C-95.4942 164.077 -93.3416 165.904 -90.7513 166.357L-55.0176 172.706C-52.4438 173.16 -49.8384 172.243 -48.1459 170.277L7.78091 105.312C9.38986 103.443 9.9219 100.859 9.19295 98.4784L-0.121262 68.3108C-0.312151 67.685 -0.709083 66.451 -1.15346 65.038C-2.10493 62.0499 -5.95909 61.2434 -7.96773 63.6127C-8.83292 64.6177 -9.62228 65.5346 -10.108 66.0988L-60.0016 123.84C-62.6428 126.908 -67.6474 125.923 -69.0239 122.076L-94.7117 49.962C-95.6405 47.3623 -97.8669 45.4406 -100.563 44.9116L-175.582 30.1016C-179.585 29.3052 -181.286 24.5021 -178.654 21.4264L-128.974 -36.4991C-128.534 -37.0104 -127.738 -37.9527 -126.836 -39.0182C-124.843 -41.3698 -126.214 -45.0429 -129.288 -45.5289C-130.666 -45.7503 -131.918 -45.9554 -132.631 -46.0639L-163.803 -50.8412C-166.262 -51.212 -168.728 -50.3134 -170.345 -48.4356L-226.272 16.5299C-227.964 18.4959 -228.493 21.2206 -227.657 23.6936L-227.666 23.6859Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M222.706 -89.7842L180.997 -98.1873C177.593 -98.8668 174.787 -95.572 176.025 -92.3217L191.188 -52.2355C192.196 -49.5833 195.663 -48.9413 197.5 -51.0746L225.064 -83.093C227.105 -85.4644 225.798 -89.1745 222.713 -89.7931L222.706 -89.7842Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M137.345 -163.463L151.793 -123.394C152.966 -120.123 150.125 -116.859 146.732 -117.605L104.902 -126.713C102.132 -127.321 100.988 -130.66 102.824 -132.793L130.388 -164.811C132.429 -167.183 136.286 -166.436 137.352 -163.471L137.345 -163.463Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M25.0397 -121.207L36.5611 -86.7521C37.3993 -84.2614 39.5133 -82.3906 42.0832 -81.8782L122.912 -65.7728C125.482 -65.2602 127.602 -63.4306 128.487 -60.9608L156.327 16.7144C157.212 19.1843 159.364 21.0115 161.955 21.4648L197.688 27.8132C200.262 28.2676 202.868 27.3509 204.56 25.3849L260.487 -39.5807C262.096 -41.4496 262.628 -44.0332 261.899 -46.4142L252.585 -76.5818C252.394 -77.2076 251.997 -78.4415 251.553 -79.8546C250.601 -82.8427 246.747 -83.6492 244.738 -81.2799C243.873 -80.2749 243.084 -79.358 242.598 -78.7937L192.704 -21.0526C190.063 -17.9846 185.059 -18.9695 183.682 -22.8162L157.994 -94.9306C157.066 -97.5303 154.839 -99.452 152.143 -99.9809L77.1236 -114.791C73.1209 -115.587 71.4197 -120.391 74.052 -123.466L123.732 -181.392C124.172 -181.903 124.968 -182.845 125.87 -183.911C127.863 -186.262 126.492 -189.935 123.418 -190.422C122.04 -190.643 120.788 -190.848 120.075 -190.956L88.9031 -195.734C86.4439 -196.105 83.9776 -195.206 82.361 -193.328L26.4342 -128.363C24.7417 -126.397 24.213 -123.672 25.0486 -121.199L25.0397 -121.207Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M30.3106 204.966L72.0193 213.369C75.4237 214.048 78.229 210.753 76.9913 207.503L61.828 167.417C60.8203 164.765 57.3527 164.123 55.5162 166.256L27.9526 198.274C25.9111 200.646 27.2187 204.356 30.303 204.975L30.3106 204.966Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M115.672 278.644L101.223 238.576C100.05 235.304 102.891 232.04 106.284 232.787L148.115 241.894C150.884 242.502 152.029 245.841 150.192 247.975L122.628 279.993C120.587 282.364 116.73 281.617 115.664 278.653L115.672 278.644Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M227.977 236.388L216.455 201.934C215.617 199.443 213.503 197.572 210.933 197.06L130.104 180.954C127.534 180.442 125.415 178.612 124.529 176.142L96.6897 98.467C95.8044 95.9972 93.6518 94.17 91.0615 93.7167L55.3278 87.3682C52.754 86.9138 50.1486 87.8306 48.4561 89.7966L-7.47073 154.762C-9.07968 156.631 -9.61172 159.215 -8.88277 161.596L0.431442 191.763C0.622332 192.389 1.01926 193.623 1.46364 195.036C2.41511 198.024 6.26927 198.831 8.27791 196.461C9.1431 195.456 9.93247 194.539 10.4182 193.975L60.3118 136.234C62.953 133.166 67.9575 134.151 69.3341 137.998L95.0219 210.112C95.9507 212.712 98.1771 214.633 100.873 215.162L175.893 229.972C179.895 230.769 181.597 235.572 178.964 238.648L129.284 296.573C128.844 297.084 128.048 298.027 127.147 299.092C125.153 301.444 126.524 305.117 129.598 305.603C130.976 305.824 132.228 306.029 132.942 306.138L164.113 310.915C166.572 311.286 169.039 310.387 170.655 308.51L226.582 243.544C228.274 241.578 228.803 238.853 227.968 236.38L227.977 236.388Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M203.002 55.1083L161.294 46.7053C157.889 46.0258 155.084 49.3206 156.322 52.5708L171.485 92.657C172.493 95.3093 175.96 95.9513 177.797 93.8179L205.361 61.7996C207.402 59.4282 206.094 55.7181 203.01 55.0995L203.002 55.1083Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M117.642 -18.57L132.09 21.4985C133.263 24.7699 130.422 28.034 127.029 27.2872L85.1985 18.1796C82.4292 17.5719 81.2845 14.2328 83.121 12.0995L110.685 -19.9189C112.726 -22.2903 116.583 -21.543 117.649 -18.5788L117.642 -18.57Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
<path
|
||||
d="M5.33659 23.6859L16.858 58.1405C17.6961 60.6312 19.8102 62.5019 22.3801 63.0144L103.209 79.1198C105.779 79.6324 107.898 81.4619 108.784 83.9318L136.623 161.607C137.509 164.077 139.661 165.904 142.252 166.357L177.985 172.706C180.559 173.16 183.165 172.243 184.857 170.277L240.784 105.312C242.393 103.443 242.925 100.859 242.196 98.4784L232.882 68.3108C232.691 67.685 232.294 66.451 231.849 65.038C230.898 62.0499 227.044 61.2434 225.035 63.6127C224.17 64.6177 223.381 65.5346 222.895 66.0988L173.001 123.84C170.36 126.908 165.356 125.923 163.979 122.076L138.291 49.962C137.362 47.3623 135.136 45.4406 132.44 44.9116L57.4205 30.1016C53.4178 29.3052 51.7166 24.5021 54.3488 21.4264L104.029 -36.4991C104.469 -37.0104 105.265 -37.9527 106.167 -39.0182C108.16 -41.3698 106.789 -45.0429 103.715 -45.5289C102.337 -45.7503 101.085 -45.9554 100.372 -46.0639L69.2 -50.8412C66.7408 -51.212 64.2744 -50.3134 62.6579 -48.4356L6.73106 16.5299C5.0386 18.4959 4.5099 21.2206 5.34549 23.6936L5.33659 23.6859Z"
|
||||
stroke="currentColor" stroke-opacity="0.18" stroke-width="2"
|
||||
mask="url(#path-1-inside-1_9092_23263)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="72" height="72" rx="12" fill="currentColor" fill-opacity="0.12" />
|
||||
<path
|
||||
d="M22.562 46.8959L31.189 42.2804C31.8922 41.9026 31.8873 40.9333 31.1763 40.558L22.426 35.9147C21.8463 35.6082 21.1377 36.0016 21.1373 36.6321L21.1319 46.0958C21.1315 46.7967 21.9244 47.2404 22.562 46.8985L22.562 46.8959Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M48.7804 47.0911L40.1588 42.3472C39.4561 41.9589 39.4621 40.9896 40.1735 40.625L48.9288 36.112C49.5092 35.8141 50.2172 36.218 50.2168 36.8485L50.2114 46.3122C50.211 47.0131 49.4178 47.445 48.7804 47.0937L48.7804 47.0911Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M62.1735 23.588L54.919 19.3778C54.3937 19.0742 53.7374 19.0615 53.2066 19.3468L36.5152 28.3219C35.9844 28.6072 35.3333 28.6024 34.8028 28.3092L18.1193 19.0882C17.5888 18.7951 16.9323 18.7954 16.4069 19.0937L9.14477 23.1933C8.62214 23.4891 8.30179 24.0235 8.30145 24.6046L8.29042 43.8064C8.2901 44.3589 8.58797 44.877 9.07744 45.1777L15.2929 48.9763C15.4214 49.0554 15.6784 49.2085 15.971 49.3853C16.5917 49.7573 17.3935 49.3359 17.3884 48.6401C17.3886 48.3431 17.3887 48.0721 17.3888 47.9053L17.4315 30.8117C17.432 29.9049 18.4636 29.3471 19.2894 29.8041L34.7506 38.3879C35.3084 38.697 35.9922 38.7021 36.5477 38.4013L52.0186 30.0477C52.845 29.603 53.8731 30.1762 53.8753 31.083L53.8983 48.177C53.8982 48.3282 53.9008 48.6044 53.9033 48.9171C53.9084 49.6077 54.707 50.0358 55.3225 49.6729C55.599 49.5108 55.8509 49.3642 55.9931 49.2792L62.2128 45.5732C62.7025 45.2798 63.0011 44.7687 63.0014 44.2137L63.0125 25.0118C63.0128 24.4307 62.693 23.8889 62.1707 23.588L62.1735 23.588Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user