Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a2ad61b8 |
No files matched your search
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
# Windows has stack overflows when calling from Tauri, so we increase compiler size
|
||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
+2
-2
@@ -14,5 +14,5 @@ max_line_length = 100
|
|||||||
max_line_length = off
|
max_line_length = off
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.{rs,java,kts}]
|
[*.rs]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
@@ -9,18 +9,14 @@ on:
|
|||||||
- .github/workflows/theseus-release.yml
|
- .github/workflows/theseus-release.yml
|
||||||
- 'apps/app/**'
|
- 'apps/app/**'
|
||||||
- 'apps/app-frontend/**'
|
- 'apps/app-frontend/**'
|
||||||
|
- 'apps/labrinth/src/common/**'
|
||||||
|
- 'apps/labrinth/Cargo.toml'
|
||||||
- 'packages/app-lib/**'
|
- 'packages/app-lib/**'
|
||||||
- 'packages/app-macros/**'
|
- 'packages/app-macros/**'
|
||||||
- 'packages/assets/**'
|
- 'packages/assets/**'
|
||||||
- 'packages/ui/**'
|
- 'packages/ui/**'
|
||||||
- 'packages/utils/**'
|
- 'packages/utils/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
sign-windows-binaries:
|
|
||||||
description: Sign Windows binaries
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
required: false
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -36,16 +32,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Rust setup (mac)
|
- name: Rust setup (mac)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
rustflags: ''
|
components: rustfmt, clippy
|
||||||
target: x86_64-apple-darwin
|
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||||
|
|
||||||
- name: Rust setup
|
- name: Rust setup
|
||||||
if: "!startsWith(matrix.platform, 'macos')"
|
if: "!startsWith(matrix.platform, 'macos')"
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
rustflags: ''
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Setup rust cache
|
- name: Setup rust cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -76,10 +72,10 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-rust-target-
|
${{ runner.os }}-rust-target-
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version: 20
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: Install pnpm via corepack
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -107,21 +103,11 @@ jobs:
|
|||||||
sudo apt-get update
|
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
|
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 code signing client (Windows only)
|
|
||||||
if: startsWith(matrix.platform, 'windows')
|
|
||||||
run: choco install jsign --ignore-dependencies # GitHub runners come with a global Java installation already
|
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Disable Windows code signing for non-final release builds
|
|
||||||
if: ${{ startsWith(matrix.platform, 'windows') && !startsWith(github.ref, 'refs/tags/v') && !inputs.sign-windows-binaries }}
|
|
||||||
run: |
|
|
||||||
jq 'del(.bundle.windows.signCommand)' apps/app/tauri-release.conf.json > apps/app/tauri-release.conf.json.new
|
|
||||||
Move-Item -Path apps/app/tauri-release.conf.json.new -Destination apps/app/tauri-release.conf.json -Force
|
|
||||||
|
|
||||||
- name: build app (macos)
|
- name: build app (macos)
|
||||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config "tauri-release.conf.json"
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -135,30 +121,15 @@ jobs:
|
|||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: build app (Linux)
|
- name: build app
|
||||||
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
|
run: pnpm --filter=@modrinth/app run tauri build --config "tauri-release.conf.json"
|
||||||
if: startsWith(matrix.platform, 'ubuntu')
|
id: build_os
|
||||||
|
if: "!startsWith(matrix.platform, 'macos')"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: build app (Windows)
|
|
||||||
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
|
|
||||||
if: startsWith(matrix.platform, 'windows')
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
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 ${{ matrix.platform }}
|
- name: upload ${{ matrix.platform }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: ['main']
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
merge_group:
|
merge_group:
|
||||||
@@ -10,69 +10,71 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Lint and Test
|
name: Build, Test, and Lint
|
||||||
runs-on: ubuntu-22.04
|
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:
|
steps:
|
||||||
- name: 📥 Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: 🧰 Install build dependencies
|
- 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
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: 🧰 Install pnpm
|
- name: Setup Node.JS environment
|
||||||
uses: pnpm/action-setup@v4
|
|
||||||
|
|
||||||
- name: 🧰 Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version: 20
|
||||||
cache: pnpm
|
|
||||||
|
|
||||||
- name: 🧰 Setup Rust toolchain
|
- name: Install pnpm via corepack
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
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
|
||||||
with:
|
with:
|
||||||
rustflags: ''
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
components: clippy, rustfmt
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
cache: false
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
- name: 🧰 Setup nextest
|
- name: Install dependencies
|
||||||
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: AlexTMjugador/cache-cargo-install-action@feat/features-support
|
|
||||||
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
|
run: pnpm install
|
||||||
|
|
||||||
- name: ⚙️ Start services
|
- name: Build
|
||||||
run: docker compose up --wait
|
run: pnpm build
|
||||||
|
env:
|
||||||
|
SQLX_OFFLINE: true
|
||||||
|
|
||||||
- name: ⚙️ Setup Labrinth environment and database
|
- name: Lint
|
||||||
working-directory: apps/labrinth
|
run: pnpm lint
|
||||||
run: |
|
env:
|
||||||
cp .env.local .env
|
SQLX_OFFLINE: true
|
||||||
sqlx database setup
|
|
||||||
|
|
||||||
- name: 🔍 Lint and test
|
- name: Start docker compose
|
||||||
run: pnpm run ci
|
run: docker compose up -d
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm test
|
||||||
|
env:
|
||||||
|
SQLX_OFFLINE: true
|
||||||
|
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
||||||
Generated
+1
-1
@@ -17,4 +17,4 @@
|
|||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
Generated
+1
-1
@@ -5,4 +5,4 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
-6
@@ -1,11 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
Generated
+267
-153
File diff suppressed because it is too large.
Load diff
+25
-69
@@ -10,9 +10,6 @@ members = [
|
|||||||
"packages/daedalus",
|
"packages/daedalus",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
actix-files = "0.6.6"
|
actix-files = "0.6.6"
|
||||||
@@ -24,8 +21,7 @@ actix-web-prom = "0.10.0"
|
|||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
async_zip = "0.0.17"
|
async-compression = { version = "0.4.23", default-features = false }
|
||||||
async-compression = { version = "0.4.24", default-features = false }
|
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
"runtime-tokio-hyper-rustls",
|
"runtime-tokio-hyper-rustls",
|
||||||
@@ -35,14 +31,14 @@ async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
|||||||
"futures-03-sink",
|
"futures-03-sink",
|
||||||
] }
|
] }
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
|
async_zip = "0.0.17"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.0"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
censor = "0.3.0"
|
censor = "0.3.0"
|
||||||
chardetng = "0.1.17"
|
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
clap = "4.5.40"
|
clap = "4.5.38"
|
||||||
clickhouse = "0.13.3"
|
clickhouse = "0.13.2"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
console-subscriber = "0.4.1"
|
console-subscriber = "0.4.1"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
@@ -54,18 +50,16 @@ dotenv-build = "0.1.1"
|
|||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
encoding_rs = "0.8.35"
|
|
||||||
enumset = "1.1.6"
|
enumset = "1.1.6"
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.1"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = { version = "0.3.31", default-features = false }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
hashlink = "0.10.0"
|
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper-tls = "0.6.0"
|
hyper-tls = "0.6.0"
|
||||||
hyper-util = "0.1.14"
|
hyper-util = "0.1.11"
|
||||||
iana-time-zone = "0.1.63"
|
iana-time-zone = "0.1.63"
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||||
indexmap = "2.9.0"
|
indexmap = "2.9.0"
|
||||||
@@ -73,7 +67,7 @@ indicatif = "0.17.11"
|
|||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jemalloc_pprof = "0.7.0"
|
jemalloc_pprof = "0.7.0"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.0.0", default-features = false }
|
||||||
lettre = { version = "0.11.17", default-features = false, features = [
|
lettre = { version = "0.11.16", default-features = false, features = [
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
@@ -95,19 +89,19 @@ quartz_nbt = "0.2.9"
|
|||||||
quick-xml = "0.37.5"
|
quick-xml = "0.37.5"
|
||||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
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
|
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
redis = "0.31.0"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.19", default-features = false }
|
reqwest = { version = "0.12.15", default-features = false }
|
||||||
rust_decimal = { version = "1.37.1", features = [
|
|
||||||
"serde-with-float",
|
|
||||||
"serde-with-str",
|
|
||||||
] }
|
|
||||||
rust_iso3166 = "0.1.14"
|
|
||||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||||
"fail-on-err",
|
"fail-on-err",
|
||||||
"tags",
|
"tags",
|
||||||
"tokio-rustls-tls",
|
"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"
|
rusty-money = "0.4.1"
|
||||||
sentry = { version = "0.38.1", default-features = false, features = [
|
sentry = { version = "0.38.1", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
@@ -119,26 +113,26 @@ sentry = { version = "0.38.1", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
sentry-actix = "0.38.1"
|
sentry-actix = "0.38.1"
|
||||||
serde = "1.0.219"
|
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_bytes = "0.11.17"
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
serde_ini = "0.2.0"
|
serde_ini = "0.2.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
serde_with = "3.12.0"
|
serde_with = "3.12.0"
|
||||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
spdx = "0.10.8"
|
spdx = "0.10.8"
|
||||||
sqlx = { version = "0.8.6", default-features = false }
|
sqlx = { version = "0.8.5", default-features = false }
|
||||||
sysinfo = { version = "0.35.2", default-features = false }
|
sysinfo = { version = "0.35.1", default-features = false }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tauri = "2.5.1"
|
tauri = "2.5.1"
|
||||||
tauri-build = "2.2.0"
|
tauri-build = "2.2.0"
|
||||||
tauri-plugin-deep-link = "2.3.0"
|
tauri-plugin-deep-link = "2.2.1"
|
||||||
tauri-plugin-dialog = "2.2.2"
|
tauri-plugin-dialog = "2.2.1"
|
||||||
tauri-plugin-opener = "2.2.7"
|
tauri-plugin-opener = "2.2.6"
|
||||||
tauri-plugin-os = "2.2.1"
|
tauri-plugin-os = "2.2.1"
|
||||||
tauri-plugin-single-instance = "2.2.4"
|
tauri-plugin-single-instance = "2.2.3"
|
||||||
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"zip",
|
"zip",
|
||||||
@@ -149,7 +143,7 @@ theseus = { path = "packages/app-lib" }
|
|||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tikv-jemalloc-ctl = "0.6.0"
|
tikv-jemalloc-ctl = "0.6.0"
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
tokio = "1.45.1"
|
tokio = "1.45.0"
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.15"
|
||||||
totp-rs = "5.7.0"
|
totp-rs = "5.7.0"
|
||||||
@@ -159,14 +153,14 @@ tracing-error = "0.2.1"
|
|||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = "1.17.0"
|
uuid = "1.16.0"
|
||||||
validator = "0.20.0"
|
validator = "0.20.0"
|
||||||
webp = { version = "0.3.0", default-features = false }
|
webp = { version = "0.3.0", default-features = false }
|
||||||
whoami = "1.6.0"
|
whoami = "1.6.0"
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
woothee = "0.13.0"
|
woothee = "0.13.0"
|
||||||
yaserde = "0.12.0"
|
yaserde = "0.12.0"
|
||||||
zip = { version = "4.0.0", default-features = false, features = [
|
zip = { version = "3.0.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -174,44 +168,6 @@ zip = { version = "4.0.0", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
zxcvbn = "3.1.0"
|
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]
|
[patch.crates-io]
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,18 @@
|
|||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"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": {
|
"dependencies": {
|
||||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
"@modrinth/ui": "workspace:*",
|
"@modrinth/ui": "workspace:*",
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
"@sentry/vue": "^8.27.0",
|
"@sentry/vue": "^8.27.0",
|
||||||
|
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||||
"@vintl/vintl": "^4.4.1",
|
"@vintl/vintl": "^4.4.1",
|
||||||
@@ -40,12 +39,11 @@
|
|||||||
"@eslint/compat": "^1.1.1",
|
"@eslint/compat": "^1.1.1",
|
||||||
"@formatjs/cli": "^6.2.12",
|
"@formatjs/cli": "^6.2.12",
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
"@taijased/vue-render-tracker": "^1.0.7",
|
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-config-custom": "workspace:*",
|
"eslint-config-custom": "workspace:*",
|
||||||
"eslint-plugin-turbo": "^2.5.4",
|
"eslint-plugin-turbo": "^2.1.1",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.74.1",
|
"sass": "^1.74.1",
|
||||||
@@ -53,7 +51,8 @@
|
|||||||
"tsconfig": "workspace:*",
|
"tsconfig": "workspace:*",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.6",
|
||||||
"vue-tsc": "^2.1.6"
|
"vue-tsc": "^2.1.6",
|
||||||
|
"@taijased/vue-render-tracker": "^1.0.7"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.4.0",
|
"packageManager": "pnpm@9.4.0",
|
||||||
"web-types": "../../web-types.json"
|
"web-types": "../../web-types.json"
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ async function handleJavaFileInput() {
|
|||||||
const filePath = await open()
|
const filePath = await open()
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
|
let result = await get_jre(filePath.path ?? filePath)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = {
|
result = {
|
||||||
path: filePath.path ?? filePath,
|
path: filePath.path ?? filePath,
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ const editProfileObject = computed(() => {
|
|||||||
hooks?: Hooks
|
hooks?: Hooks
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
// When hooks are not overridden per-instance, we want to clear them
|
if (overrideHooks.value) {
|
||||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
editProfile.hooks = hooks.value
|
||||||
|
}
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
||||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
import {
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
set_world_display_status,
|
||||||
|
getWorldIdentifier,
|
||||||
|
showWorldInFolder,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
|
import { formatNumber } from '@modrinth/utils'
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -45,7 +49,6 @@ const router = useRouter()
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||||
(e: 'open-folder', world: SingleplayerWorld): void
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -105,6 +108,20 @@ const serverIncompatible = computed(
|
|||||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -377,7 +394,8 @@ const messages = defineMessages({
|
|||||||
{
|
{
|
||||||
id: 'open-folder',
|
id: 'open-folder',
|
||||||
shown: world.type === 'singleplayer',
|
shown: world.type === 'singleplayer',
|
||||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
action: () =>
|
||||||
|
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
divider: true,
|
divider: true,
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ function show(world: SingleplayerWorld) {
|
|||||||
icon.value = world.icon
|
icon.value = world.icon
|
||||||
displayStatus.value = world.display_status
|
displayStatus.value = world.display_status
|
||||||
hideFromHome.value = world.display_status === 'hidden'
|
hideFromHome.value = world.display_status === 'hidden'
|
||||||
removeIcon.value = false
|
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,7 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-if="
|
v-if="instance.install_stage.includes('installing')"
|
||||||
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
|
||||||
instance.install_stage,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
color="brand"
|
color="brand"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -86,7 +86,6 @@
|
|||||||
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
||||||
"
|
"
|
||||||
@delete="() => promptToRemoveWorld(world)"
|
@delete="() => promptToRemoveWorld(world)"
|
||||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,7 +151,6 @@ import {
|
|||||||
hasQuickPlaySupport,
|
hasQuickPlaySupport,
|
||||||
refreshWorlds,
|
refreshWorlds,
|
||||||
handleDefaultProfileUpdateEvent,
|
handleDefaultProfileUpdateEvent,
|
||||||
showWorldInFolder,
|
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_playground"
|
name = "theseus_playground"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
edition.workspace = true
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -9,6 +9,3 @@ edition.workspace = true
|
|||||||
theseus = { workspace = true, features = ["cli"] }
|
theseus = { workspace = true, features = ["cli"] }
|
||||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
enumset.workspace = true
|
enumset.workspace = true
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
"name": "@modrinth/app-playground",
|
"name": "@modrinth/app-playground",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
"fix": "cargo fmt && cargo clippy --fix",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
"test": "cargo test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-5
@@ -4,7 +4,8 @@ version = "0.9.5"
|
|||||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/modrinth/code/apps/app/"
|
repository = "https://github.com/modrinth/code/apps/app/"
|
||||||
edition.workspace = true
|
edition = "2024"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { workspace = true, features = ["codegen"] }
|
tauri-build = { workspace = true, features = ["codegen"] }
|
||||||
@@ -16,7 +17,7 @@ serde_json.workspace = true
|
|||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
|
|
||||||
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset"] }
|
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||||
tauri-plugin-window-state.workspace = true
|
tauri-plugin-window-state.workspace = true
|
||||||
tauri-plugin-deep-link.workspace = true
|
tauri-plugin-deep-link.workspace = true
|
||||||
tauri-plugin-os.workspace = true
|
tauri-plugin-os.workspace = true
|
||||||
@@ -55,6 +56,3 @@ default = ["custom-protocol"]
|
|||||||
# DO NOT remove this
|
# DO NOT remove this
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
updater = []
|
updater = []
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app",
|
"name": "@modrinth/app",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tauri": "tauri",
|
|
||||||
"build": "tauri build",
|
"build": "tauri build",
|
||||||
|
"tauri": "tauri",
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"test": "cargo nextest run --all-targets --no-fail-fast",
|
"test": "cargo test",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
||||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt"
|
"fix": "cargo fmt && cargo clippy --fix"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "2.5.0"
|
"@tauri-apps/cli": "2.5.0"
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ pub async fn jre_find_filtered_jres(
|
|||||||
// Validates JRE at a given path
|
// Validates JRE at a given path
|
||||||
// Returns None if the path is not a valid JRE
|
// Returns None if the path is not a valid JRE
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn jre_get_jre(path: PathBuf) -> Result<JavaVersion> {
|
pub async fn jre_get_jre(path: PathBuf) -> Result<Option<JavaVersion>> {
|
||||||
Ok(jre::check_jre(path).await?)
|
jre::check_jre(path).await.map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests JRE of a certain version
|
// Tests JRE of a certain version
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ pub fn get_os() -> OS {
|
|||||||
let os = OS::MacOS;
|
let os = OS::MacOS;
|
||||||
os
|
os
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum OS {
|
pub enum OS {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub async fn get_recent_worlds<R: Runtime>(
|
|||||||
display_statuses.unwrap_or(EnumSet::all()),
|
display_statuses.unwrap_or(EnumSet::all()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for world in &mut result {
|
for world in result.iter_mut() {
|
||||||
adapt_world_icon(&app_handle, &mut world.world);
|
adapt_world_icon(&app_handle, &mut world.world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -55,7 +55,7 @@ pub async fn get_profile_worlds<R: Runtime>(
|
|||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<Vec<World>> {
|
) -> Result<Vec<World>> {
|
||||||
let mut result = worlds::get_profile_worlds(path).await?;
|
let mut result = worlds::get_profile_worlds(path).await?;
|
||||||
for world in &mut result {
|
for world in result.iter_mut() {
|
||||||
adapt_world_icon(&app_handle, world);
|
adapt_world_icon(&app_handle, world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager: &M,
|
manager: &M,
|
||||||
) -> InitialPayload {
|
) -> InitialPayload {
|
||||||
let initial_payload = manager.try_state::<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()
|
initial_payload.inner().clone()
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("No initial payload found, creating new");
|
tracing::info!("No initial payload found, creating new");
|
||||||
@@ -23,5 +22,7 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager.manage(payload.clone());
|
manager.manage(payload.clone());
|
||||||
|
|
||||||
payload
|
payload
|
||||||
}
|
};
|
||||||
|
|
||||||
|
mtx
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ fn main() {
|
|||||||
{
|
{
|
||||||
let payload = macos::deep_link::get_or_init_payload(app);
|
let payload = macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload;
|
let mtx_copy = payload.payload.clone();
|
||||||
app.listen("deep-link://new-url", move |url| {
|
app.listen("deep-link://new-url", move |url| {
|
||||||
let mtx_copy_copy = mtx_copy.clone();
|
let mtx_copy_copy = mtx_copy.clone();
|
||||||
let request = url.payload().to_owned();
|
let request = url.payload().to_owned();
|
||||||
@@ -229,6 +229,7 @@ fn main() {
|
|||||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||||
payload,
|
payload,
|
||||||
));
|
));
|
||||||
|
dbg!(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
@@ -272,22 +273,22 @@ fn main() {
|
|||||||
|
|
||||||
match app {
|
match app {
|
||||||
Ok(app) => {
|
Ok(app) => {
|
||||||
|
#[allow(unused_variables)]
|
||||||
app.run(|app, event| {
|
app.run(|app, event| {
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
drop((app, event));
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let tauri::RunEvent::Opened { urls } = event {
|
if let tauri::RunEvent::Opened { urls } = event {
|
||||||
tracing::info!("Handling webview open {urls:?}");
|
tracing::info!("Handling webview open {urls:?}");
|
||||||
|
|
||||||
let file = urls
|
let file = urls
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|url| url.to_file_path().ok());
|
.filter_map(|url| url.to_file_path().ok())
|
||||||
|
.next();
|
||||||
|
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
let payload =
|
let payload =
|
||||||
macos::deep_link::get_or_init_payload(app);
|
macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload;
|
let mtx_copy = payload.payload.clone();
|
||||||
let request = file.to_string_lossy().to_string();
|
let request = file.to_string_lossy().to_string();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut payload = mtx_copy.lock().await;
|
let mut payload = mtx_copy.lock().await;
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
{
|
{
|
||||||
"bundle": {
|
"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": {
|
"build": {
|
||||||
"features": ["updater"]
|
"features": ["updater"]
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
"externalBin": [],
|
"externalBin": [],
|
||||||
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||||
"windows": {
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": "http://timestamp.digicert.com",
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"installMode": "perMachine",
|
"installMode": "perMachine",
|
||||||
"installerHooks": "./nsis/hooks.nsi"
|
"installerHooks": "./nsis/hooks.nsi"
|
||||||
@@ -27,6 +30,7 @@
|
|||||||
"providerShortName": null,
|
"providerShortName": null,
|
||||||
"signingIdentity": null
|
"signingIdentity": null
|
||||||
},
|
},
|
||||||
|
"resources": [],
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
"linux": {
|
"linux": {
|
||||||
"deb": {
|
"deb": {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"$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"
|
name = "daedalus_client"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
authors = ["Jai A <jai@modrinth.com>"]
|
authors = ["Jai A <jai@modrinth.com>"]
|
||||||
edition.workspace = true
|
edition = "2024"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -28,6 +28,3 @@ tracing-error.workspace = true
|
|||||||
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM rust:1.87.0 AS build
|
FROM rust:1.86.0 AS build
|
||||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
|
||||||
WORKDIR /usr/src/daedalus
|
WORKDIR /usr/src/daedalus
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
"name": "@modrinth/daedalus_client",
|
"name": "@modrinth/daedalus_client",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
"fix": "cargo fmt && cargo clippy --fix",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
"test": "cargo test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/daedalus": "workspace:*"
|
"@modrinth/daedalus": "workspace:*"
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ pub async fn fetch(
|
|||||||
if modrinth_version
|
if modrinth_version
|
||||||
.original_sha1
|
.original_sha1
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|x| x == &version.sha1)
|
.map(|x| x == &version.sha1)
|
||||||
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
existing_versions.push(modrinth_version);
|
existing_versions.push(modrinth_version);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"lint": "astro check",
|
"build": "astro check && astro build",
|
||||||
"build": "astro build",
|
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
@@ -19,4 +18,4 @@
|
|||||||
"starlight-openapi": "^0.14.0",
|
"starlight-openapi": "^0.14.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,10 +85,11 @@ During development, you might notice that changes made directly to entities in t
|
|||||||
|
|
||||||
#### CDN options
|
#### CDN options
|
||||||
|
|
||||||
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local` or `s3`, but defaults to `local`
|
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local`, `backblaze`, or `s3`, but defaults to `local`
|
||||||
|
|
||||||
The S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
The Backblaze and 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`
|
`BACKBLAZE_KEY_ID`, `BACKBLAZE_KEY`, `BACKBLAZE_BUCKET_ID`
|
||||||
|
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_BUCKET_NAME`
|
||||||
|
|
||||||
#### Search, OAuth, and miscellaneous options
|
#### Search, OAuth, and miscellaneous options
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ BASE_URL=http://127.0.0.1:8000/v2/
|
|||||||
BROWSER_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
|
PYRO_BASE_URL=https://staging-archon.modrinth.com
|
||||||
PROD_OVERRIDE=true
|
PROD_OVERRIDE=true
|
||||||
|
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
"postinstall": "nuxi prepare",
|
"postinstall": "nuxi prepare",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"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": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^6.2.12",
|
"@formatjs/cli": "^6.2.12",
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<OmorphiaAvatar
|
||||||
|
:src="src"
|
||||||
|
:alt="alt"
|
||||||
|
:size="size"
|
||||||
|
:circle="circle"
|
||||||
|
:no-shadow="noShadow"
|
||||||
|
:loading="loading"
|
||||||
|
:raised="raised"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Avatar as OmorphiaAvatar } from "@modrinth/ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: "2rem",
|
||||||
|
},
|
||||||
|
circle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
noShadow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: String,
|
||||||
|
default: "eager",
|
||||||
|
},
|
||||||
|
raised: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
'badge flex items-center gap-1 font-semibold text-secondary ' + color + ' type--' + type
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template v-if="color"> <span class="circle" /> {{ capitalizeString(type) }}</template>
|
||||||
|
|
||||||
|
<!-- User roles -->
|
||||||
|
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team</template>
|
||||||
|
<template v-else-if="type === 'moderator'"> <ModeratorIcon /> Moderator</template>
|
||||||
|
<template v-else-if="type === 'creator'"><CreatorIcon /> Creator</template>
|
||||||
|
<template v-else-if="type === 'plus'"><PlusIcon /> Modrinth Plus</template>
|
||||||
|
|
||||||
|
<!-- Project statuses -->
|
||||||
|
<template v-else-if="type === 'approved'"><GlobeIcon /> Public</template>
|
||||||
|
<template v-else-if="type === 'approved-general'"><CheckIcon /> Approved</template>
|
||||||
|
<template v-else-if="type === 'unlisted' || type === 'withheld'"
|
||||||
|
><LinkIcon /> Unlisted</template
|
||||||
|
>
|
||||||
|
<template v-else-if="type === 'private'"><LockIcon /> Private</template>
|
||||||
|
<template v-else-if="type === 'scheduled'"> <CalendarIcon /> Scheduled</template>
|
||||||
|
<template v-else-if="type === 'draft'"><DraftIcon /> Draft</template>
|
||||||
|
<template v-else-if="type === 'archived'"> <ArchiveIcon /> Archived</template>
|
||||||
|
<template v-else-if="type === 'rejected'"><CrossIcon /> Rejected</template>
|
||||||
|
<template v-else-if="type === 'processing'"> <ProcessingIcon /> Under review</template>
|
||||||
|
|
||||||
|
<!-- Team members -->
|
||||||
|
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
|
||||||
|
<template v-else-if="type === 'pending'"> <ProcessingIcon /> Pending </template>
|
||||||
|
|
||||||
|
<!-- Transaction statuses -->
|
||||||
|
<template v-else-if="type === 'success'"><CheckIcon /> Success</template>
|
||||||
|
|
||||||
|
<!-- Report status -->
|
||||||
|
<template v-else-if="type === 'closed'"> <CloseIcon /> Closed</template>
|
||||||
|
|
||||||
|
<!-- Other -->
|
||||||
|
<template v-else> <span class="circle" /> {{ capitalizeString(type) }} </template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
GlobeIcon,
|
||||||
|
LinkIcon,
|
||||||
|
ModrinthIcon,
|
||||||
|
PlusIcon,
|
||||||
|
ScaleIcon as ModeratorIcon,
|
||||||
|
BoxIcon as CreatorIcon,
|
||||||
|
FileTextIcon as DraftIcon,
|
||||||
|
XIcon as CrossIcon,
|
||||||
|
ArchiveIcon,
|
||||||
|
UpdatedIcon as ProcessingIcon,
|
||||||
|
CheckIcon,
|
||||||
|
LockIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
XCircleIcon as CloseIcon,
|
||||||
|
} from "@modrinth/assets";
|
||||||
|
import { capitalizeString } from "@modrinth/utils";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.badge {
|
||||||
|
.circle {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
background-color: var(--badge-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
vertical-align: -15%;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type--closed,
|
||||||
|
&.type--withheld,
|
||||||
|
&.type--rejected,
|
||||||
|
&.red {
|
||||||
|
--badge-color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type--pending,
|
||||||
|
&.type--moderator,
|
||||||
|
&.type--processing,
|
||||||
|
&.type--scheduled,
|
||||||
|
&.orange {
|
||||||
|
--badge-color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type--accepted,
|
||||||
|
&.type--admin,
|
||||||
|
&.type--success,
|
||||||
|
&.type--approved-general,
|
||||||
|
&.green {
|
||||||
|
--badge-color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type--creator,
|
||||||
|
&.blue {
|
||||||
|
--badge-color: var(--color-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type--unlisted,
|
||||||
|
&.type--plus,
|
||||||
|
&.purple {
|
||||||
|
--badge-color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type--private,
|
||||||
|
&.type--approved,
|
||||||
|
&.gray {
|
||||||
|
--badge-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
|
||||||
|
<span>{{ text }}</span>
|
||||||
|
<CheckIcon v-if="copied" />
|
||||||
|
<ClipboardCopyIcon v-else />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { CheckIcon, ClipboardCopyIcon } from "@modrinth/assets";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CheckIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
copied: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async copyText() {
|
||||||
|
await navigator.clipboard.writeText(this.text);
|
||||||
|
this.copied = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.code {
|
||||||
|
color: var(--color-text);
|
||||||
|
display: inline-flex;
|
||||||
|
grid-gap: 0.5rem;
|
||||||
|
font-family: var(--mono-font);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: var(--color-code-bg);
|
||||||
|
width: fit-content;
|
||||||
|
border-radius: 10px;
|
||||||
|
user-select: text;
|
||||||
|
transition:
|
||||||
|
opacity 0.5s ease-in-out,
|
||||||
|
filter 0.2s ease-in-out,
|
||||||
|
transform 0.05s ease-in-out,
|
||||||
|
outline 0.2s ease-in-out;
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -654,11 +654,11 @@ For a brief rundown of how this works:
|
|||||||
{
|
{
|
||||||
name: "Insufficient",
|
name: "Insufficient",
|
||||||
resultingMessage: `## Insufficient Gallery Images
|
resultingMessage: `## Insufficient Gallery Images
|
||||||
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations).
|
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations).
|
||||||
Keep in mind that you should:
|
Keep in mind that you should:
|
||||||
- Set a featured image that best represents your project.
|
- Set a featured image that best represents your project.
|
||||||
- Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description.
|
- Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description.
|
||||||
- Upload any relevant images in your Description to your Gallery tab for best results.`,
|
- Upload any relevant images in your Description to your Gallery tab for best results.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Not relevant",
|
name: "Not relevant",
|
||||||
|
|||||||
@@ -104,13 +104,13 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
||||||
has been
|
has been
|
||||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
<Badge :type="notification.body.new_status" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
updated from
|
updated from
|
||||||
<ProjectStatusBadge :status="notification.body.old_status" />
|
<Badge :type="notification.body.old_status" />
|
||||||
to
|
to
|
||||||
<ProjectStatusBadge :status="notification.body.new_status" />
|
<Badge :type="notification.body.new_status" />
|
||||||
</template>
|
</template>
|
||||||
by the moderators.
|
by the moderators.
|
||||||
</template>
|
</template>
|
||||||
@@ -331,13 +331,16 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
import { useRelativeTime } from "@modrinth/ui";
|
||||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||||
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
||||||
import { getUserLink } from "~/helpers/users.js";
|
import { getUserLink } from "~/helpers/users.js";
|
||||||
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
||||||
import { markAsRead } from "~/helpers/notifications.ts";
|
import { markAsRead } from "~/helpers/notifications.ts";
|
||||||
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
||||||
|
import Avatar from "~/components/ui/Avatar.vue";
|
||||||
|
import Badge from "~/components/ui/Badge.vue";
|
||||||
|
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||||
import Categories from "~/components/ui/search/Categories.vue";
|
import Categories from "~/components/ui/search/Categories.vue";
|
||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="vue-notification-group experimental-styles-within">
|
||||||
class="vue-notification-group experimental-styles-within"
|
|
||||||
:class="{ 'intercom-present': isIntercomPresent }"
|
|
||||||
>
|
|
||||||
<transition-group name="notifs">
|
<transition-group name="notifs">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in notifications"
|
v-for="(item, index) in notifications"
|
||||||
@@ -83,8 +80,6 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
const notifications = useNotifications();
|
const notifications = useNotifications();
|
||||||
|
|
||||||
const isIntercomPresent = ref(false);
|
|
||||||
|
|
||||||
function stopTimer(notif) {
|
function stopTimer(notif) {
|
||||||
clearTimeout(notif.timer);
|
clearTimeout(notif.timer);
|
||||||
}
|
}
|
||||||
@@ -111,27 +106,6 @@ const createNotifText = (notif) => {
|
|||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkIntercomPresence() {
|
|
||||||
isIntercomPresent.value = !!document.querySelector(".intercom-lightweight-app");
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
checkIntercomPresence();
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
checkIntercomPresence();
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
observer.disconnect();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function copyToClipboard(notif) {
|
function copyToClipboard(notif) {
|
||||||
const text = createNotifText(notif);
|
const text = createNotifText(notif);
|
||||||
|
|
||||||
@@ -156,10 +130,6 @@ function copyToClipboard(notif) {
|
|||||||
bottom: 0.75rem;
|
bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.intercom-present {
|
|
||||||
bottom: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue-notification-wrapper {
|
.vue-notification-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
<template>
|
|
||||||
<nav
|
|
||||||
ref="scrollContainer"
|
|
||||||
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-for="(option, index) in options"
|
|
||||||
:key="`option-group-${index}`"
|
|
||||||
ref="optionButtons"
|
|
||||||
class="button-animation z-[1] flex flex-row items-center gap-2 rounded-full bg-transparent px-4 py-2 font-semibold"
|
|
||||||
:class="{
|
|
||||||
'text-button-textSelected': modelValue === option,
|
|
||||||
'text-primary': modelValue !== option,
|
|
||||||
}"
|
|
||||||
@click="setOption(option)"
|
|
||||||
>
|
|
||||||
<slot :option="option" :selected="modelValue === option" />
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
class="navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full bg-button-bgSelected p-1"
|
|
||||||
:style="{
|
|
||||||
left: sliderLeftPx,
|
|
||||||
top: sliderTopPx,
|
|
||||||
right: sliderRightPx,
|
|
||||||
bottom: sliderBottomPx,
|
|
||||||
opacity: initialized ? 1 : 0,
|
|
||||||
}"
|
|
||||||
aria-hidden="true"
|
|
||||||
></div>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts" generic="T">
|
|
||||||
import { ref, computed, onMounted } from "vue";
|
|
||||||
|
|
||||||
const modelValue = defineModel<T>({ required: true });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
options: T[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const scrollContainer = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
const sliderLeft = ref(4);
|
|
||||||
const sliderTop = ref(4);
|
|
||||||
const sliderRight = ref(4);
|
|
||||||
const sliderBottom = ref(4);
|
|
||||||
|
|
||||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
|
|
||||||
const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
|
||||||
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
|
||||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
|
|
||||||
|
|
||||||
const optionButtons = ref();
|
|
||||||
|
|
||||||
const initialized = ref(false);
|
|
||||||
|
|
||||||
function setOption(option: T) {
|
|
||||||
modelValue.value = option;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(modelValue, () => {
|
|
||||||
startAnimation(props.options.indexOf(modelValue.value));
|
|
||||||
});
|
|
||||||
|
|
||||||
function startAnimation(index: number) {
|
|
||||||
const el = optionButtons.value[index];
|
|
||||||
|
|
||||||
if (!el || !el.offsetParent) return;
|
|
||||||
|
|
||||||
const newValues = {
|
|
||||||
left: el.offsetLeft,
|
|
||||||
top: el.offsetTop,
|
|
||||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
|
||||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
|
||||||
sliderLeft.value = newValues.left;
|
|
||||||
sliderRight.value = newValues.right;
|
|
||||||
sliderTop.value = newValues.top;
|
|
||||||
sliderBottom.value = newValues.bottom;
|
|
||||||
} else {
|
|
||||||
const delay = 200;
|
|
||||||
|
|
||||||
if (newValues.left < sliderLeft.value) {
|
|
||||||
sliderLeft.value = newValues.left;
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderRight.value = newValues.right;
|
|
||||||
}, delay);
|
|
||||||
} else {
|
|
||||||
sliderRight.value = newValues.right;
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderLeft.value = newValues.left;
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValues.top < sliderTop.value) {
|
|
||||||
sliderTop.value = newValues.top;
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderBottom.value = newValues.bottom;
|
|
||||||
}, delay);
|
|
||||||
} else {
|
|
||||||
sliderBottom.value = newValues.bottom;
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderTop.value = newValues.top;
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initialized.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
startAnimation(props.options.indexOf(modelValue.value));
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.navtabs-transition {
|
|
||||||
transition:
|
|
||||||
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
||||||
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-shadow {
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<div class="table-cell">
|
<div class="table-cell">
|
||||||
<BoxIcon />
|
<BoxIcon />
|
||||||
<span>{{
|
<span>{{
|
||||||
formatProjectType(
|
$formatProjectType(
|
||||||
$getProjectTypeForDisplay(
|
$getProjectTypeForDisplay(
|
||||||
project.project_types?.[0] ?? "project",
|
project.project_types?.[0] ?? "project",
|
||||||
project.loaders,
|
project.loaders,
|
||||||
@@ -111,7 +111,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
||||||
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
|
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
|
||||||
import { formatProjectType } from "@modrinth/utils";
|
|
||||||
|
|
||||||
const modalOpen = ref(null);
|
const modalOpen = ref(null);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="count > 1" class="columns paginates">
|
||||||
|
<a
|
||||||
|
:class="{ disabled: page === 1 }"
|
||||||
|
:tabindex="page === 1 ? -1 : 0"
|
||||||
|
class="left-arrow paginate has-icon"
|
||||||
|
aria-label="Previous Page"
|
||||||
|
:href="linkFunction(page - 1)"
|
||||||
|
@click.prevent="page !== 1 ? switchPage(page - 1) : null"
|
||||||
|
>
|
||||||
|
<LeftArrowIcon />
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in pages"
|
||||||
|
:key="'page-' + item + '-' + index"
|
||||||
|
:class="{
|
||||||
|
'page-number': page !== item,
|
||||||
|
shrink: item > 99,
|
||||||
|
}"
|
||||||
|
class="page-number-container"
|
||||||
|
>
|
||||||
|
<div v-if="item === '-'" class="has-icon">
|
||||||
|
<GapIcon />
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:class="{
|
||||||
|
'page-number current': page === item,
|
||||||
|
shrink: item > 99,
|
||||||
|
}"
|
||||||
|
:href="linkFunction(item)"
|
||||||
|
@click.prevent="page !== item ? switchPage(item) : null"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:class="{
|
||||||
|
disabled: page === pages[pages.length - 1],
|
||||||
|
}"
|
||||||
|
:tabindex="page === pages[pages.length - 1] ? -1 : 0"
|
||||||
|
class="right-arrow paginate has-icon"
|
||||||
|
aria-label="Next Page"
|
||||||
|
:href="linkFunction(page + 1)"
|
||||||
|
@click.prevent="page !== pages[pages.length - 1] ? switchPage(page + 1) : null"
|
||||||
|
>
|
||||||
|
<RightArrowIcon />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { GapIcon, LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GapIcon,
|
||||||
|
LeftArrowIcon,
|
||||||
|
RightArrowIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
linkFunction: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return () => "/";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["switch-page"],
|
||||||
|
computed: {
|
||||||
|
pages() {
|
||||||
|
let pages = [];
|
||||||
|
|
||||||
|
if (this.count > 7) {
|
||||||
|
if (this.page + 3 >= this.count) {
|
||||||
|
pages = [
|
||||||
|
1,
|
||||||
|
"-",
|
||||||
|
this.count - 4,
|
||||||
|
this.count - 3,
|
||||||
|
this.count - 2,
|
||||||
|
this.count - 1,
|
||||||
|
this.count,
|
||||||
|
];
|
||||||
|
} else if (this.page > 5) {
|
||||||
|
pages = [1, "-", this.page - 1, this.page, this.page + 1, "-", this.count];
|
||||||
|
} else {
|
||||||
|
pages = [1, 2, 3, 4, 5, "-", this.count];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages = Array.from({ length: this.count }, (_, i) => i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
switchPage(newPage) {
|
||||||
|
this.$emit("switch-page", newPage);
|
||||||
|
if (newPage !== null && newPage !== "" && !isNaN(newPage)) {
|
||||||
|
this.$emit("switch-page", Math.min(Math.max(newPage, 1), this.count));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
a {
|
||||||
|
position: relative;
|
||||||
|
color: var(--color-button-text);
|
||||||
|
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
||||||
|
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 2rem;
|
||||||
|
background: var(--color-raised-bg);
|
||||||
|
|
||||||
|
transition:
|
||||||
|
opacity 0.5s ease-in-out,
|
||||||
|
filter 0.2s ease-in-out,
|
||||||
|
transform 0.05s ease-in-out,
|
||||||
|
outline 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&.page-number.current {
|
||||||
|
background: var(--color-brand);
|
||||||
|
color: var(--color-brand-inverted);
|
||||||
|
cursor: default;
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.paginate.disabled {
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: not-allowed;
|
||||||
|
filter: grayscale(50%);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.has-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-number-container,
|
||||||
|
a,
|
||||||
|
.has-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paginates {
|
||||||
|
height: 2em;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
|
> div,
|
||||||
|
.has-icon {
|
||||||
|
margin: 0 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-arrow {
|
||||||
|
margin-left: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-arrow {
|
||||||
|
margin-right: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.paginates {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 530px) {
|
||||||
|
a {
|
||||||
|
width: 2.5rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
{{ author }}
|
{{ author }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<ProjectStatusBadge v-if="status && status !== 'approved'" :status="status" class="status" />
|
<Badge v-if="status && status !== 'approved'" :type="status" class="status" />
|
||||||
</div>
|
</div>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
@@ -91,16 +91,18 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets";
|
import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets";
|
||||||
import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
|
|
||||||
import Categories from "~/components/ui/search/Categories.vue";
|
import Categories from "~/components/ui/search/Categories.vue";
|
||||||
|
import Badge from "~/components/ui/Badge.vue";
|
||||||
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
||||||
|
import Avatar from "~/components/ui/Avatar.vue";
|
||||||
|
import { useRelativeTime } from "@modrinth/ui";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ProjectStatusBadge,
|
|
||||||
EnvironmentIndicator,
|
EnvironmentIndicator,
|
||||||
Avatar,
|
Avatar,
|
||||||
Categories,
|
Categories,
|
||||||
|
Badge,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ import {
|
|||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { formatProjectType } from "@modrinth/utils";
|
import { formatProjectType } from "~/plugins/shorthands.js";
|
||||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
|||||||
props,
|
props,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const returnTopN = 15;
|
const returnTopN = 5;
|
||||||
|
|
||||||
const listEntries = series
|
const listEntries = series
|
||||||
.map((value, index) => [
|
.map((value, index) => [
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
<div class="stacked">
|
<div class="stacked">
|
||||||
<span class="title">{{ report.project.title }}</span>
|
<span class="title">{{ report.project.title }}</span>
|
||||||
<span>{{
|
<span>{{
|
||||||
formatProjectType(
|
$formatProjectType(
|
||||||
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
<div class="stacked">
|
<div class="stacked">
|
||||||
<span class="title">{{ report.project.title }}</span>
|
<span class="title">{{ report.project.title }}</span>
|
||||||
<span>{{
|
<span>{{
|
||||||
formatProjectType(
|
$formatProjectType(
|
||||||
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,11 +104,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
||||||
import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
|
||||||
import { formatProjectType } from "@modrinth/utils";
|
|
||||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||||
|
import { useRelativeTime } from "@modrinth/ui";
|
||||||
|
import Avatar from "~/components/ui/Avatar.vue";
|
||||||
|
import Badge from "~/components/ui/Badge.vue";
|
||||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||||
import { getProjectTypeForUrl } from "~/helpers/projects.js";
|
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||||
import { addReportMessage } from "~/helpers/threads.js";
|
import { addReportMessage } from "~/helpers/threads.js";
|
||||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
moderation: {
|
moderation: {
|
||||||
@@ -54,13 +53,13 @@ const threadIds = [
|
|||||||
|
|
||||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||||
fetchSegmented(userIds, (ids) => `users?ids=${asEncodedJsonArray(ids)}`),
|
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
||||||
),
|
),
|
||||||
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
||||||
fetchSegmented(versionIds, (ids) => `versions?ids=${asEncodedJsonArray(ids)}`),
|
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`),
|
||||||
),
|
),
|
||||||
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||||
fetchSegmented(threadIds, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ const versionProjects = versions.value.map((version) => version.project_id);
|
|||||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
|
const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
|
||||||
|
|
||||||
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
||||||
fetchSegmented(projectIds, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
reports.value = rawReports.map((report) => {
|
reports.value = rawReports.map((report) => {
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
<span
|
<span
|
||||||
v-for="category in categoriesFiltered"
|
v-for="category in categoriesFiltered"
|
||||||
:key="category.name"
|
:key="category.name"
|
||||||
v-html="category.icon + formatCategory(category.name)"
|
v-html="category.icon + $formatCategory(category.name)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { formatCategory } from "@modrinth/utils";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
categories: {
|
categories: {
|
||||||
@@ -40,7 +38,6 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: { formatCategory },
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,9 @@
|
|||||||
import { ref, nextTick, computed } from "vue";
|
import { ref, nextTick, computed } from "vue";
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
|
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
|
||||||
import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils";
|
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
@@ -66,7 +64,7 @@ const trimmedName = computed(() => backupName.value.trim());
|
|||||||
const nameExists = computed(() => {
|
const nameExists = computed(() => {
|
||||||
if (!props.server.backups?.data) return false;
|
if (!props.server.backups?.data) return false;
|
||||||
return props.server.backups.data.some(
|
return props.server.backups.data.some(
|
||||||
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,7 +98,7 @@ const createBackup = async () => {
|
|||||||
hideModal();
|
hideModal();
|
||||||
await props.server.refresh();
|
await props.server.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||||
isRateLimited.value = true;
|
isRateLimited.value = true;
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -20,9 +20,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { ConfirmModal } from "@modrinth/ui";
|
import { ConfirmModal } from "@modrinth/ui";
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "delete", backup: Backup | undefined): void;
|
(e: "delete", backup: Backup | undefined): void;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
||||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||||
import { ref, computed } from "vue";
|
import { ref } from "vue";
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Backup } from "~/composables/pyroServers.ts";
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags();
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
@@ -52,10 +52,9 @@ const failedToCreate = computed(() => props.backup.interrupted);
|
|||||||
const preparedDownloadStates = ["ready", "done"];
|
const preparedDownloadStates = ["ready", "done"];
|
||||||
const inactiveStates = ["failed", "cancelled"];
|
const inactiveStates = ["failed", "cancelled"];
|
||||||
|
|
||||||
const hasPreparedDownload = computed(() => {
|
const hasPreparedDownload = computed(() =>
|
||||||
const fileState = props.backup.task?.file?.state ?? "";
|
preparedDownloadStates.includes(props.backup.task?.file?.state ?? ""),
|
||||||
return preparedDownloadStates.includes(fileState);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const creating = computed(() => {
|
const creating = computed(() => {
|
||||||
const task = props.backup.task?.create;
|
const task = props.backup.task?.create;
|
||||||
@@ -82,10 +81,6 @@ const restoring = computed(() => {
|
|||||||
const initiatedPrepare = ref(false);
|
const initiatedPrepare = ref(false);
|
||||||
|
|
||||||
const preparingFile = computed(() => {
|
const preparingFile = computed(() => {
|
||||||
if (hasPreparedDownload.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const task = props.backup.task?.file;
|
const task = props.backup.task?.file;
|
||||||
return (
|
return (
|
||||||
(!task && initiatedPrepare.value) ||
|
(!task && initiatedPrepare.value) ||
|
||||||
|
|||||||
@@ -48,11 +48,10 @@
|
|||||||
import { ref, nextTick, computed } from "vue";
|
import { ref, nextTick, computed } from "vue";
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
@@ -71,7 +70,7 @@ const nameExists = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return props.server.backups.data.some(
|
return props.server.backups.data.some(
|
||||||
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { ConfirmModal, NewModal } from "@modrinth/ui";
|
import { ConfirmModal, NewModal } from "@modrinth/ui";
|
||||||
import type { Backup } from "@modrinth/utils";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
|
|||||||
@@ -59,10 +59,10 @@
|
|||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { XIcon, SaveIcon } from "@modrinth/assets";
|
import { XIcon, SaveIcon } from "@modrinth/assets";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["backups"]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ import {
|
|||||||
import { Admonition, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { Admonition, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
|
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils";
|
import { formatCategory, formatVersionsForDisplay, type Version } from "@modrinth/utils";
|
||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import Accordion from "~/components/ui/Accordion.vue";
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||||
import ContentVersionFilter, {
|
import ContentVersionFilter, {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
/>
|
/>
|
||||||
<XCircleIcon
|
<XCircleIcon
|
||||||
v-show="
|
v-show="
|
||||||
item.status.includes('error') ||
|
item.status === 'error' ||
|
||||||
item.status === 'cancelled' ||
|
item.status === 'cancelled' ||
|
||||||
item.status === 'incorrect-type'
|
item.status === 'incorrect-type'
|
||||||
"
|
"
|
||||||
@@ -54,14 +54,9 @@
|
|||||||
<template v-if="item.status === 'completed'">
|
<template v-if="item.status === 'completed'">
|
||||||
<span>Done</span>
|
<span>Done</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="item.status === 'error-file-exists'">
|
<template v-else-if="item.status === 'error'">
|
||||||
<span class="text-red">Failed - File already exists</span>
|
<span class="text-red">Failed - File already exists</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="item.status === 'error-generic'">
|
|
||||||
<span class="text-red"
|
|
||||||
>Failed - {{ item.error?.message || "An unexpected error occured." }}</span
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="item.status === 'incorrect-type'">
|
<template v-else-if="item.status === 'incorrect-type'">
|
||||||
<span class="text-red">Failed - Incorrect file type</span>
|
<span class="text-red">Failed - Incorrect file type</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -104,22 +99,14 @@
|
|||||||
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
|
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { ref, computed, watch, nextTick } from "vue";
|
import { ref, computed, watch, nextTick } from "vue";
|
||||||
import { FSModule } from "~/composables/servers/modules/fs.ts";
|
import type { FSModule } from "~/composables/pyroServers.ts";
|
||||||
|
|
||||||
interface UploadItem {
|
interface UploadItem {
|
||||||
file: File;
|
file: File;
|
||||||
progress: number;
|
progress: number;
|
||||||
status:
|
status: "pending" | "uploading" | "completed" | "error" | "cancelled" | "incorrect-type";
|
||||||
| "pending"
|
|
||||||
| "uploading"
|
|
||||||
| "completed"
|
|
||||||
| "error-file-exists"
|
|
||||||
| "error-generic"
|
|
||||||
| "cancelled"
|
|
||||||
| "incorrect-type";
|
|
||||||
size: string;
|
size: string;
|
||||||
uploader?: any;
|
uploader?: any;
|
||||||
error?: Error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -258,18 +245,8 @@ const uploadFile = async (file: File) => {
|
|||||||
console.error("Error uploading file:", error);
|
console.error("Error uploading file:", error);
|
||||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||||
const target = uploadQueue.value[index];
|
uploadQueue.value[index].status =
|
||||||
|
error instanceof Error && error.message === badFileTypeMsg ? "incorrect-type" : "error";
|
||||||
if (error instanceof Error) {
|
|
||||||
if (error.message === badFileTypeMsg) {
|
|
||||||
target.status = "incorrect-type";
|
|
||||||
} else if (target.progress === 100 && error.message.includes("401")) {
|
|
||||||
target.status = "error-file-exists";
|
|
||||||
} else {
|
|
||||||
target.status = "error-generic";
|
|
||||||
target.error = error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|||||||
@@ -75,14 +75,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ExternalIcon, SpinnerIcon, DownloadIcon, XIcon } from "@modrinth/assets";
|
import { ExternalIcon, SpinnerIcon, DownloadIcon, XIcon } from "@modrinth/assets";
|
||||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
|
||||||
import { ref, computed, nextTick } from "vue";
|
import { ref, computed, nextTick } from "vue";
|
||||||
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import { handleError, type Server } from "~/composables/pyroServers.ts";
|
||||||
|
|
||||||
const cf = ref(false);
|
const cf = ref(false);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>();
|
||||||
@@ -111,18 +110,24 @@ const handleSubmit = async () => {
|
|||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
// hide();
|
// hide();
|
||||||
try {
|
try {
|
||||||
const dry = await props.server.fs.extractFile(trimmedUrl.value, true, true);
|
const dry = await props.server.fs?.extractFile(trimmedUrl.value, true, true);
|
||||||
|
|
||||||
if (!cf.value || dry.modpack_name) {
|
if (!cf.value || dry.modpack_name) {
|
||||||
await props.server.fs.extractFile(trimmedUrl.value, true, false, true);
|
await props.server.fs?.extractFile(trimmedUrl.value, true, false, true);
|
||||||
hide();
|
hide();
|
||||||
} else {
|
} else {
|
||||||
submitted.value = false;
|
submitted.value = false;
|
||||||
handleError(
|
handleError(
|
||||||
new ModrinthServersFetchError(
|
new ServersError(
|
||||||
"Could not find CurseForge modpack at that URL.",
|
"Could not find CurseForge modpack at that URL.",
|
||||||
404,
|
undefined,
|
||||||
new Error(`No modpack found at ${url.value}`),
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
context: "Error installing modpack",
|
||||||
|
error: `url: ${url.value}`,
|
||||||
|
description: "Could not find CurseForge modpack at that URL.",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const props = defineProps<{
|
|||||||
loader: string | null;
|
loader: string | null;
|
||||||
loader_version: string | null;
|
loader_version: string | null;
|
||||||
};
|
};
|
||||||
ignoreCurrentInstallation?: boolean;
|
|
||||||
isInstalling?: boolean;
|
isInstalling?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
style="font-variant-numeric: tabular-nums"
|
||||||
|
class="pointer-events-none h-full w-full select-none"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="flex flex-row items-center gap-6">
|
||||||
|
<div
|
||||||
|
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||||
|
>
|
||||||
|
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
|
||||||
|
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
|
||||||
|
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
|
||||||
|
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
|
||||||
|
</div>
|
||||||
|
<h3 class="relative z-10 text-base font-normal text-secondary">CPU usage</h3>
|
||||||
|
</div>
|
||||||
|
<CPUIcon class="absolute right-10 top-10" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||||
|
>
|
||||||
|
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
|
||||||
|
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
|
||||||
|
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
|
||||||
|
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
|
||||||
|
</div>
|
||||||
|
<h3 class="relative z-10 text-base font-normal text-secondary">Memory usage</h3>
|
||||||
|
</div>
|
||||||
|
<DBIcon class="absolute right-10 top-10" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">0 B</h2>
|
||||||
|
</div>
|
||||||
|
<h3 class="relative z-10 text-base font-normal text-secondary">Storage usage</h3>
|
||||||
|
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative flex h-full w-full flex-col gap-3 overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||||
|
>
|
||||||
|
<div class="experimental-styles-within flex flex-row items-center">
|
||||||
|
<div class="flex flex-row items-center gap-4">
|
||||||
|
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-full">
|
||||||
|
<input type="text" placeholder="Search logs" class="h-12 !w-full !pl-10 !pr-48" />
|
||||||
|
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="console relative h-full min-h-[516px] w-full overflow-hidden rounded-xl bg-bg text-sm"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CPUIcon, DBIcon, FolderOpenIcon, SearchIcon } from "@modrinth/assets";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
html.light-mode .console {
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark-mode .console {
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.oled-mode .console {
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ButtonStyled v-if="showStopButton" type="transparent">
|
<ButtonStyled v-if="showStopButton" type="transparent">
|
||||||
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
<button :disabled="!canTakeAction" @click="initiateAction('stop')">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<StopCircleIcon class="h-5 w-5" />
|
<StopCircleIcon class="h-5 w-5" />
|
||||||
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
||||||
@@ -120,12 +120,14 @@ import {
|
|||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils";
|
|
||||||
|
type ServerAction = "start" | "stop" | "restart" | "kill";
|
||||||
|
type ServerState = "stopped" | "starting" | "running" | "stopping" | "restarting";
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
interface PowerAction {
|
interface PowerAction {
|
||||||
action: ServerPowerAction;
|
action: ServerAction;
|
||||||
nextState: ServerState;
|
nextState: ServerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +142,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "action", action: ServerPowerAction): void;
|
(e: "action", action: ServerAction): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -168,7 +170,7 @@ const isStoppingState = computed(() => serverState.value === "stopping");
|
|||||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
||||||
|
|
||||||
const primaryActionText = computed(() => {
|
const primaryActionText = computed(() => {
|
||||||
const states: Partial<Record<ServerState, string>> = {
|
const states: Record<ServerState, string> = {
|
||||||
starting: "Starting...",
|
starting: "Starting...",
|
||||||
restarting: "Restarting...",
|
restarting: "Restarting...",
|
||||||
running: "Restart",
|
running: "Restart",
|
||||||
@@ -191,7 +193,7 @@ const menuOptions = computed(() => [
|
|||||||
id: "kill",
|
id: "kill",
|
||||||
label: "Kill server",
|
label: "Kill server",
|
||||||
icon: SlashIcon,
|
icon: SlashIcon,
|
||||||
action: () => initiateAction("Kill"),
|
action: () => initiateAction("kill"),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
@@ -219,17 +221,17 @@ async function copyId() {
|
|||||||
await navigator.clipboard.writeText(serverId as string);
|
await navigator.clipboard.writeText(serverId as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initiateAction(action: ServerPowerAction) {
|
function initiateAction(action: ServerAction) {
|
||||||
if (!canTakeAction.value) return;
|
if (!canTakeAction.value) return;
|
||||||
|
|
||||||
const stateMap: Record<ServerPowerAction, ServerState> = {
|
const stateMap: Record<ServerAction, ServerState> = {
|
||||||
Start: "starting",
|
start: "starting",
|
||||||
Stop: "stopping",
|
stop: "stopping",
|
||||||
Restart: "restarting",
|
restart: "restarting",
|
||||||
Kill: "stopping",
|
kill: "stopping",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (action === "Start") {
|
if (action === "start") {
|
||||||
emit("action", action);
|
emit("action", action);
|
||||||
serverState.value = stateMap[action];
|
serverState.value = stateMap[action];
|
||||||
startingDelay.value = true;
|
startingDelay.value = true;
|
||||||
@@ -247,7 +249,7 @@ function initiateAction(action: ServerPowerAction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePrimaryAction() {
|
function handlePrimaryAction() {
|
||||||
initiateAction(isRunning.value ? "Restart" : "Start");
|
initiateAction(isRunning.value ? "restart" : "start");
|
||||||
}
|
}
|
||||||
|
|
||||||
function executePowerAction() {
|
function executePowerAction() {
|
||||||
@@ -261,7 +263,7 @@ function executePowerAction() {
|
|||||||
userPreferences.value.powerDontAskAgain = true;
|
userPreferences.value.powerDontAskAgain = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "Start") {
|
if (action === "start") {
|
||||||
startingDelay.value = true;
|
startingDelay.value = true;
|
||||||
setTimeout(() => (startingDelay.value = false), 5000);
|
setTimeout(() => (startingDelay.value = false), 5000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { ServerState } from "@modrinth/utils";
|
import type { ServerState } from "~/types/servers";
|
||||||
|
|
||||||
const STATUS_CLASSES = {
|
const STATUS_CLASSES = {
|
||||||
running: { main: "bg-brand", bg: "bg-bg-green" },
|
running: { main: "bg-brand", bg: "bg-bg-green" },
|
||||||
@@ -49,7 +49,7 @@ const STATUS_CLASSES = {
|
|||||||
unknown: { main: "", bg: "" },
|
unknown: { main: "", bg: "" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
|
const STATUS_TEXTS = {
|
||||||
running: "Running",
|
running: "Running",
|
||||||
stopped: "",
|
stopped: "",
|
||||||
crashed: "Crashed",
|
crashed: "Crashed",
|
||||||
@@ -63,10 +63,7 @@ defineProps<{
|
|||||||
const isExpanded = ref(false);
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
function getStatusClass(state: ServerState) {
|
function getStatusClass(state: ServerState) {
|
||||||
if (state in STATUS_CLASSES) {
|
return STATUS_CLASSES[state] ?? STATUS_CLASSES.unknown;
|
||||||
return STATUS_CLASSES[state as keyof typeof STATUS_CLASSES];
|
|
||||||
}
|
|
||||||
return STATUS_CLASSES.unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusText(state: ServerState) {
|
function getStatusText(state: ServerState) {
|
||||||
|
|||||||
@@ -7,17 +7,16 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search logs"
|
placeholder="Search logs"
|
||||||
class="h-12 !w-full !pl-10 !pr-48"
|
class="h-12 !w-full !pl-10 !pr-48"
|
||||||
:disabled="loading"
|
|
||||||
@keydown.escape="clearSearch"
|
@keydown.escape="clearSearch"
|
||||||
/>
|
/>
|
||||||
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
|
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
|
||||||
<ButtonStyled v-if="searchInput && !loading" @click="clearSearch">
|
<ButtonStyled v-if="searchInput" @click="clearSearch">
|
||||||
<button class="absolute right-2 top-1/2 -translate-y-1/2">
|
<button class="absolute right-2 top-1/2 -translate-y-1/2">
|
||||||
<XIcon class="h-5 w-5" />
|
<XIcon class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<span
|
<span
|
||||||
v-if="pyroConsole.filteredOutput.value.length && searchInput && !loading"
|
v-if="pyroConsole.filteredOutput.value.length && searchInput"
|
||||||
class="pointer-events-none absolute right-12 top-1/2 -translate-y-1/2 select-none whitespace-pre text-sm"
|
class="pointer-events-none absolute right-12 top-1/2 -translate-y-1/2 select-none whitespace-pre text-sm"
|
||||||
>
|
>
|
||||||
{{ pyroConsole.filteredOutput.value.length }}
|
{{ pyroConsole.filteredOutput.value.length }}
|
||||||
@@ -30,13 +29,11 @@
|
|||||||
:class="[
|
:class="[
|
||||||
'terminal-font console relative z-[1] flex h-full w-full flex-col items-center justify-between overflow-hidden rounded-t-xl px-1 text-sm transition-transform duration-300',
|
'terminal-font console relative z-[1] flex h-full w-full flex-col items-center justify-between overflow-hidden rounded-t-xl px-1 text-sm transition-transform duration-300',
|
||||||
{ 'scale-fullscreen screen-fixed inset-0 z-50 !rounded-none': isFullScreen },
|
{ 'scale-fullscreen screen-fixed inset-0 z-50 !rounded-none': isFullScreen },
|
||||||
{ 'pointer-events-none': loading },
|
|
||||||
]"
|
]"
|
||||||
:aria-hidden="loading"
|
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="cosmetics.advancedRendering && !loading"
|
v-if="cosmetics.advancedRendering"
|
||||||
class="progressive-gradient pointer-events-none absolute -bottom-6 left-0 z-[2] h-[10rem] w-full overflow-hidden rounded-xl"
|
class="progressive-gradient pointer-events-none absolute -bottom-6 left-0 z-[2] h-[10rem] w-full overflow-hidden rounded-xl"
|
||||||
:style="`--transparency: ${Math.max(0, lerp(100, 0, bottomThreshold * 8))}%`"
|
:style="`--transparency: ${Math.max(0, lerp(100, 0, bottomThreshold * 8))}%`"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -50,7 +47,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="!loading"
|
v-else
|
||||||
class="pointer-events-none absolute bottom-0 left-0 right-0 z-[2] h-[196px] w-full"
|
class="pointer-events-none absolute bottom-0 left-0 right-0 z-[2] h-[196px] w-full"
|
||||||
:style="
|
:style="
|
||||||
bottomThreshold > 0
|
bottomThreshold > 0
|
||||||
@@ -82,7 +79,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div data-pyro-terminal-scroll-root class="relative h-full w-full">
|
<div data-pyro-terminal-scroll-root class="relative h-full w-full">
|
||||||
<div
|
<div
|
||||||
v-if="!loading"
|
|
||||||
ref="scrollbarTrack"
|
ref="scrollbarTrack"
|
||||||
data-pyro-terminal-scrollbar-track
|
data-pyro-terminal-scrollbar-track
|
||||||
class="absolute -right-1 bottom-16 top-4 z-[4] w-4 overflow-hidden"
|
class="absolute -right-1 bottom-16 top-4 z-[4] w-4 overflow-hidden"
|
||||||
@@ -122,12 +118,7 @@
|
|||||||
class="scrollbar-none absolute left-0 top-0 h-full w-full select-text overflow-x-auto overflow-y-auto py-6 pb-[72px]"
|
class="scrollbar-none absolute left-0 top-0 h-full w-full select-text overflow-x-auto overflow-y-auto py-6 pb-[72px]"
|
||||||
@scroll.passive="() => handleListScroll()"
|
@scroll.passive="() => handleListScroll()"
|
||||||
>
|
>
|
||||||
<div v-if="loading" class="h-full w-full" />
|
<div data-pyro-terminal-virtual-height-watcher :style="{ height: `${totalHeight}px` }">
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
data-pyro-terminal-virtual-height-watcher
|
|
||||||
:style="{ height: `${totalHeight}px` }"
|
|
||||||
>
|
|
||||||
<ul
|
<ul
|
||||||
class="m-0 list-none p-0"
|
class="m-0 list-none p-0"
|
||||||
data-pyro-terminal-virtual-list
|
data-pyro-terminal-virtual-list
|
||||||
@@ -214,7 +205,6 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="!loading"
|
|
||||||
data-pyro-fullscreen
|
data-pyro-fullscreen
|
||||||
:label="isFullScreen ? 'Exit full screen' : 'Enter full screen'"
|
:label="isFullScreen ? 'Exit full screen' : 'Enter full screen'"
|
||||||
class="experimental-styles-within absolute right-4 top-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
class="experimental-styles-within absolute right-4 top-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
||||||
@@ -227,7 +217,7 @@
|
|||||||
|
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-if="(hasSelection || isSingleLineSelected) && !loading"
|
v-if="hasSelection || isSingleLineSelected"
|
||||||
class="absolute right-20 top-4 z-[3] flex flex-row items-center"
|
class="absolute right-20 top-4 z-[3] flex flex-row items-center"
|
||||||
:class="{ '!right-4': searchInput || hasSelection || isSingleLineSelected }"
|
:class="{ '!right-4': searchInput || hasSelection || isSingleLineSelected }"
|
||||||
>
|
>
|
||||||
@@ -257,7 +247,7 @@
|
|||||||
|
|
||||||
<Transition name="scroll-to-bottom">
|
<Transition name="scroll-to-bottom">
|
||||||
<button
|
<button
|
||||||
v-if="bottomThreshold > 0 && !isScrolledToBottom && !loading"
|
v-if="bottomThreshold > 0 && !isScrolledToBottom"
|
||||||
data-pyro-scrolltobottom
|
data-pyro-scrolltobottom
|
||||||
label="Scroll to bottom"
|
label="Scroll to bottom"
|
||||||
class="scroll-to-bottom-btn experimental-styles-within absolute bottom-[4.5rem] right-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
class="scroll-to-bottom-btn experimental-styles-within absolute bottom-[4.5rem] right-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
||||||
@@ -301,14 +291,13 @@ import { useDebounceFn } from "@vueuse/core";
|
|||||||
import { NewModal } from "@modrinth/ui";
|
import { NewModal } from "@modrinth/ui";
|
||||||
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { useModrinthServersConsole } from "~/store/console.ts";
|
import { usePyroConsole } from "~/store/console.ts";
|
||||||
|
|
||||||
const { $cosmetics } = useNuxtApp();
|
const { $cosmetics } = useNuxtApp();
|
||||||
const cosmetics = $cosmetics;
|
const cosmetics = $cosmetics;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
loading?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const BUFFER_SIZE = 5;
|
const BUFFER_SIZE = 5;
|
||||||
@@ -318,8 +307,8 @@ const SEPARATOR_HEIGHT = 32;
|
|||||||
const SCROLL_END_DELAY = 150;
|
const SCROLL_END_DELAY = 150;
|
||||||
const progressiveBlurIterations = ref(8);
|
const progressiveBlurIterations = ref(8);
|
||||||
|
|
||||||
const pyroConsole = useModrinthServersConsole();
|
const pyroConsole = usePyroConsole();
|
||||||
const consoleOutput = computed(() => (props.loading ? [] : pyroConsole.output.value));
|
const consoleOutput = pyroConsole.output;
|
||||||
|
|
||||||
const scrollContainer = ref<HTMLElement | null>(null);
|
const scrollContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { DownloadIcon, XIcon } from "@modrinth/assets";
|
import { DownloadIcon, XIcon } from "@modrinth/assets";
|
||||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
project: any;
|
project: any;
|
||||||
versions: any[];
|
versions: any[];
|
||||||
currentVersion?: any;
|
currentVersion?: any;
|
||||||
@@ -99,7 +98,8 @@ const handleReinstall = async () => {
|
|||||||
try {
|
try {
|
||||||
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id;
|
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id;
|
||||||
|
|
||||||
await props.server.general.reinstall(
|
await props.server.general?.reinstall(
|
||||||
|
props.server.serverId,
|
||||||
false,
|
false,
|
||||||
props.project.id,
|
props.project.id,
|
||||||
versionId,
|
versionId,
|
||||||
@@ -110,7 +110,7 @@ const handleReinstall = async () => {
|
|||||||
emit("reinstall");
|
emit("reinstall");
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -116,12 +116,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { UploadIcon, RightArrowIcon, XIcon, ServerIcon } from "@modrinth/assets";
|
import { UploadIcon, RightArrowIcon, XIcon, ServerIcon } from "@modrinth/assets";
|
||||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -176,7 +175,7 @@ const handleReinstall = async () => {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -127,10 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||||
v-if="!initialSetup"
|
|
||||||
class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4"
|
|
||||||
>
|
|
||||||
<div class="flex w-full flex-row items-center justify-between">
|
<div class="flex w-full flex-row items-center justify-between">
|
||||||
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
|
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
|
||||||
Erase all data
|
Erase all data
|
||||||
@@ -149,10 +146,7 @@
|
|||||||
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BackupWarning
|
<BackupWarning :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
||||||
v-if="!initialSetup"
|
|
||||||
:backup-link="`/servers/manage/${props.server?.serverId}/backups`"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-start gap-4">
|
<div class="mt-4 flex justify-start gap-4">
|
||||||
@@ -200,9 +194,9 @@
|
|||||||
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
||||||
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
||||||
import { $fetch } from "ofetch";
|
import { $fetch } from "ofetch";
|
||||||
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
|
import type { Loaders } from "~/types/servers";
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|
||||||
@@ -220,10 +214,9 @@ type VersionMap = Record<string, LoaderVersion[]>;
|
|||||||
type VersionCache = Record<string, any>;
|
type VersionCache = Record<string, any>;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
currentLoader: Loaders | undefined;
|
currentLoader: Loaders | undefined;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
initialSetup?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -320,7 +313,7 @@ const selectedLoaderVersions = computed<string[]>(() => {
|
|||||||
const loader = selectedLoader.value.toLowerCase();
|
const loader = selectedLoader.value.toLowerCase();
|
||||||
|
|
||||||
if (loader === "paper") {
|
if (loader === "paper") {
|
||||||
return paperVersions.value[selectedMCVersion.value]?.map((x) => `${x}`) || [];
|
return paperVersions.value[selectedMCVersion.value].map((x) => `${x}`) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loader === "purpur") {
|
if (loader === "purpur") {
|
||||||
@@ -458,11 +451,12 @@ const handleReinstall = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await props.server.general?.reinstall(
|
await props.server.general?.reinstall(
|
||||||
|
props.server.serverId,
|
||||||
true,
|
true,
|
||||||
selectedLoader.value,
|
selectedLoader.value,
|
||||||
selectedMCVersion.value,
|
selectedMCVersion.value,
|
||||||
selectedLoader.value === "Vanilla" ? "" : selectedLoaderVersion.value,
|
selectedLoader.value === "Vanilla" ? "" : selectedLoaderVersion.value,
|
||||||
props.initialSetup ? true : hardReset.value,
|
hardReset.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit("reinstall", {
|
emit("reinstall", {
|
||||||
@@ -473,7 +467,7 @@ const handleReinstall = async () => {
|
|||||||
|
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
|
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
import type { Server } from "~/composables/pyroServers";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
@@ -39,7 +39,7 @@ const props = defineProps<{
|
|||||||
save: () => void;
|
save: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const saveAndRestart = async () => {
|
const saveAndRestart = async () => {
|
||||||
|
|||||||
@@ -1,278 +0,0 @@
|
|||||||
<template>
|
|
||||||
<LazyUiServersPlatformVersionSelectModal
|
|
||||||
ref="versionSelectModal"
|
|
||||||
:server="props.server"
|
|
||||||
:current-loader="ignoreCurrentInstallation ? undefined : (data?.loader as Loaders)"
|
|
||||||
:backup-in-progress="backupInProgress"
|
|
||||||
:initial-setup="ignoreCurrentInstallation"
|
|
||||||
@reinstall="emit('reinstall', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LazyUiServersPlatformMrpackModal
|
|
||||||
ref="mrpackModal"
|
|
||||||
:server="props.server"
|
|
||||||
@reinstall="emit('reinstall', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LazyUiServersPlatformChangeModpackVersionModal
|
|
||||||
ref="modpackVersionModal"
|
|
||||||
:server="props.server"
|
|
||||||
:project="data?.project"
|
|
||||||
:versions="Array.isArray(versions) ? versions : []"
|
|
||||||
:current-version="currentVersion"
|
|
||||||
:current-version-id="data?.upstream?.version_id"
|
|
||||||
:server-status="data?.status"
|
|
||||||
@reinstall="emit('reinstall')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-col">
|
|
||||||
<div v-if="data && versions" class="flex w-full flex-col">
|
|
||||||
<div class="card flex flex-col gap-4">
|
|
||||||
<div class="flex select-none flex-col items-center justify-between gap-2 lg:flex-row">
|
|
||||||
<div class="flex flex-row items-center gap-2">
|
|
||||||
<h2 class="m-0 text-lg font-bold text-contrast">Modpack</h2>
|
|
||||||
<div
|
|
||||||
v-if="updateAvailable"
|
|
||||||
class="rounded-full bg-bg-orange px-2 py-1 text-xs font-medium text-orange"
|
|
||||||
>
|
|
||||||
<span>Update available</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="data.upstream" class="flex gap-4">
|
|
||||||
<ButtonStyled>
|
|
||||||
<button
|
|
||||||
class="!w-full sm:!w-auto"
|
|
||||||
:disabled="isInstalling"
|
|
||||||
@click="mrpackModal.show()"
|
|
||||||
>
|
|
||||||
<UploadIcon class="size-4" /> Import .mrpack
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<!-- dumb hack to make a button link not a link -->
|
|
||||||
<ButtonStyled>
|
|
||||||
<template v-if="isInstalling">
|
|
||||||
<button :disabled="isInstalling">
|
|
||||||
<TransferIcon class="size-4" />
|
|
||||||
Switch modpack
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<nuxt-link v-else :to="`/modpacks?sid=${props.server.serverId}`">
|
|
||||||
<TransferIcon class="size-4" />
|
|
||||||
Switch modpack
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="data.upstream" class="flex flex-col gap-2">
|
|
||||||
<div
|
|
||||||
v-if="versionsError || currentVersionError"
|
|
||||||
class="rounded-2xl border border-solid border-red p-4 text-contrast"
|
|
||||||
>
|
|
||||||
<p class="m-0 font-bold">Something went wrong while loading your modpack.</p>
|
|
||||||
<p class="m-0 mb-2 mt-1 text-sm">
|
|
||||||
{{ versionsError || currentVersionError }}
|
|
||||||
</p>
|
|
||||||
<ButtonStyled>
|
|
||||||
<button :disabled="isInstalling" @click="refreshData">Retry</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<NewProjectCard
|
|
||||||
v-if="!versionsError && !currentVersionError"
|
|
||||||
class="!cursor-default !bg-bg !filter-none"
|
|
||||||
:project="projectCardData"
|
|
||||||
:categories="data.project?.categories || []"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<ButtonStyled color="brand">
|
|
||||||
<button :disabled="isInstalling" @click="modpackVersionModal.show()">
|
|
||||||
<SettingsIcon class="size-4" />
|
|
||||||
Change version
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</template>
|
|
||||||
</NewProjectCard>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
|
|
||||||
<ButtonStyled>
|
|
||||||
<nuxt-link
|
|
||||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
|
||||||
:class="{ disabled: backupInProgress }"
|
|
||||||
class="!w-full sm:!w-auto"
|
|
||||||
:to="`/modpacks?sid=${props.server.serverId}`"
|
|
||||||
>
|
|
||||||
<CompassIcon class="size-4" /> Find a modpack
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<span class="hidden sm:block">or</span>
|
|
||||||
<ButtonStyled>
|
|
||||||
<button
|
|
||||||
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
|
||||||
:disabled="!!backupInProgress"
|
|
||||||
class="!w-full sm:!w-auto"
|
|
||||||
@click="mrpackModal.show()"
|
|
||||||
>
|
|
||||||
<UploadIcon class="size-4" /> Upload .mrpack file
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card flex flex-col gap-4">
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<h2 class="m-0 text-lg font-bold text-contrast">Platform</h2>
|
|
||||||
<p class="m-0">Your server's platform is the software that runs mods and plugins.</p>
|
|
||||||
<div v-if="data.upstream" class="mt-2 flex items-center gap-2">
|
|
||||||
<InfoIcon class="hidden sm:block" />
|
|
||||||
<span class="text-sm text-secondary">
|
|
||||||
The current platform was automatically selected based on your modpack.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex w-full flex-col gap-1 rounded-2xl"
|
|
||||||
:class="{
|
|
||||||
'pointer-events-none cursor-not-allowed select-none opacity-50':
|
|
||||||
props.server.general?.status === 'installing',
|
|
||||||
}"
|
|
||||||
:tabindex="props.server.general?.status === 'installing' ? -1 : 0"
|
|
||||||
>
|
|
||||||
<UiServersLoaderSelector
|
|
||||||
:data="
|
|
||||||
ignoreCurrentInstallation
|
|
||||||
? {
|
|
||||||
loader: null,
|
|
||||||
loader_version: null,
|
|
||||||
}
|
|
||||||
: data
|
|
||||||
"
|
|
||||||
:is-installing="isInstalling"
|
|
||||||
@select-loader="selectLoader"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
|
||||||
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
|
||||||
import type { Loaders } from "@modrinth/utils";
|
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
server: ModrinthServer;
|
|
||||||
ignoreCurrentInstallation?: boolean;
|
|
||||||
backupInProgress?: BackupInProgressReason;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
reinstall: [any?];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const isInstalling = computed(() => props.server.general?.status === "installing");
|
|
||||||
|
|
||||||
const versionSelectModal = ref();
|
|
||||||
const mrpackModal = ref();
|
|
||||||
const modpackVersionModal = ref();
|
|
||||||
|
|
||||||
const data = computed(() => props.server.general);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: versions,
|
|
||||||
error: versionsError,
|
|
||||||
refresh: refreshVersions,
|
|
||||||
} = await useAsyncData(
|
|
||||||
`content-loader-versions-${data.value?.upstream?.project_id}`,
|
|
||||||
async () => {
|
|
||||||
if (!data.value?.upstream?.project_id) return [];
|
|
||||||
try {
|
|
||||||
const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`);
|
|
||||||
return result || [];
|
|
||||||
} catch (e) {
|
|
||||||
console.error("couldnt fetch all versions:", e);
|
|
||||||
throw new Error("Failed to load modpack versions.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => [] },
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: currentVersion,
|
|
||||||
error: currentVersionError,
|
|
||||||
refresh: refreshCurrentVersion,
|
|
||||||
} = await useAsyncData(
|
|
||||||
`content-loader-version-${data.value?.upstream?.version_id}`,
|
|
||||||
async () => {
|
|
||||||
if (!data.value?.upstream?.version_id) return null;
|
|
||||||
try {
|
|
||||||
const result = await useBaseFetch(`version/${data.value.upstream.version_id}`);
|
|
||||||
return result || null;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("couldnt fetch version:", e);
|
|
||||||
throw new Error("Failed to load modpack version.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => null },
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectCardData = computed(() => ({
|
|
||||||
icon_url: data.value?.project?.icon_url,
|
|
||||||
title: data.value?.project?.title,
|
|
||||||
description: data.value?.project?.description,
|
|
||||||
downloads: data.value?.project?.downloads,
|
|
||||||
follows: data.value?.project?.followers,
|
|
||||||
// @ts-ignore
|
|
||||||
date_modified: currentVersion.value?.date_published || data.value?.project?.updated,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const selectLoader = (loader: string) => {
|
|
||||||
versionSelectModal.value?.show(loader as Loaders);
|
|
||||||
};
|
|
||||||
|
|
||||||
const refreshData = async () => {
|
|
||||||
await Promise.all([refreshVersions(), refreshCurrentVersion()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateAvailable = computed(() => {
|
|
||||||
// so sorry
|
|
||||||
// @ts-ignore
|
|
||||||
if (!data.value?.upstream || !versions.value?.length || !currentVersion.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const latestVersion = versions.value[0];
|
|
||||||
// @ts-ignore
|
|
||||||
return latestVersion.id !== currentVersion.value.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.server.general?.status,
|
|
||||||
async (newStatus, oldStatus) => {
|
|
||||||
if (oldStatus === "installing" && newStatus === "available") {
|
|
||||||
await Promise.all([
|
|
||||||
refreshVersions(),
|
|
||||||
refreshCurrentVersion(),
|
|
||||||
props.server.refresh(["general"]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.stylized-toggle:checked::after {
|
|
||||||
background: var(--color-accent-contrast) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-base:active {
|
|
||||||
scale: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -43,14 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="min-h-[20px]"></div>
|
<div v-else class="min-h-[20px]"></div>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="isConfiguring"
|
|
||||||
class="flex min-w-0 items-center gap-2 truncate text-sm font-semibold text-brand"
|
|
||||||
>
|
|
||||||
<SparklesIcon class="size-5 shrink-0" /> New server
|
|
||||||
</div>
|
|
||||||
<UiServersServerInfoLabels
|
<UiServersServerInfoLabels
|
||||||
v-else
|
|
||||||
:server-data="{ game, mc_version, loader, loader_version, net }"
|
:server-data="{ game, mc_version, loader, loader_version, net }"
|
||||||
:show-game-label="showGameLabel"
|
:show-game-label="showGameLabel"
|
||||||
:show-loader-label="showLoaderLabel"
|
:show-loader-label="showLoaderLabel"
|
||||||
@@ -80,14 +73,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
|
import { ChevronRightIcon, HammerIcon, LockIcon } from "@modrinth/assets";
|
||||||
import type { Project, Server } from "@modrinth/utils";
|
import type { Project, Server } from "~/types/servers";
|
||||||
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const props = defineProps<Partial<Server>>();
|
const props = defineProps<Partial<Server>>();
|
||||||
|
|
||||||
if (props.server_id) {
|
if (props.server_id) {
|
||||||
await useModrinthServers(props.server_id, ["general"]);
|
await usePyroServer(props.server_id, ["general"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showGameLabel = computed(() => !!props.game);
|
const showGameLabel = computed(() => !!props.game);
|
||||||
@@ -110,9 +102,8 @@ if (props.upstream) {
|
|||||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
||||||
|
|
||||||
if (import.meta.server && projectData.value?.icon_url) {
|
if (import.meta.server && projectData.value?.icon_url) {
|
||||||
await useModrinthServers(props.server_id!, ["general"]);
|
await usePyroServer(props.server_id!, ["general"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
||||||
const isConfiguring = computed(() => props.flows?.intro);
|
|
||||||
</script>
|
</script>
|
||||||
@@ -34,15 +34,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RightArrowIcon } from "@modrinth/assets";
|
import { RightArrowIcon } from "@modrinth/assets";
|
||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from "vue-router";
|
||||||
|
import type { Server } from "~/composables/pyroServers";
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
|
||||||
|
|
||||||
const emit = defineEmits(["reinstall"]);
|
const emit = defineEmits(["reinstall"]);
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
|
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
|
||||||
route: RouteLocationNormalized;
|
route: RouteLocationNormalized;
|
||||||
server: ModrinthServer;
|
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
data-pyro-server-stats
|
data-pyro-server-stats
|
||||||
style="font-variant-numeric: tabular-nums"
|
style="font-variant-numeric: tabular-nums"
|
||||||
class="flex select-none flex-col items-center gap-6 md:flex-row"
|
class="flex select-none flex-col items-center gap-6 md:flex-row"
|
||||||
:class="{ 'pointer-events-none': loading }"
|
|
||||||
:aria-hidden="loading"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(metric, index) in metrics"
|
v-for="(metric, index) in metrics"
|
||||||
@@ -20,7 +18,7 @@
|
|||||||
<h3 class="flex items-center gap-2 text-base font-normal text-secondary">
|
<h3 class="flex items-center gap-2 text-base font-normal text-secondary">
|
||||||
{{ metric.title }}
|
{{ metric.title }}
|
||||||
<IssuesIcon
|
<IssuesIcon
|
||||||
v-if="metric.warning && !loading"
|
v-if="metric.warning"
|
||||||
v-tooltip="metric.warning"
|
v-tooltip="metric.warning"
|
||||||
class="size-5"
|
class="size-5"
|
||||||
:style="{ color: 'var(--color-orange)' }"
|
:style="{ color: 'var(--color-orange)' }"
|
||||||
@@ -30,76 +28,51 @@
|
|||||||
<div class="absolute -left-8 -top-4 h-28 w-56 rounded-full bg-bg-raised blur-lg" />
|
<div class="absolute -left-8 -top-4 h-28 w-56 rounded-full bg-bg-raised blur-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component
|
<component :is="metric.icon" class="absolute right-10 top-10 z-10" />
|
||||||
:is="metric.icon"
|
<ClientOnly>
|
||||||
class="absolute right-10 top-10 z-10 size-8"
|
<VueApexCharts
|
||||||
style="width: 2rem; height: 2rem"
|
v-if="metric.showGraph"
|
||||||
/>
|
type="area"
|
||||||
|
height="142"
|
||||||
<div class="chart-space absolute bottom-0 left-0 right-0">
|
:options="getChartOptions(metric.warning)"
|
||||||
<ClientOnly>
|
:series="[{ name: metric.title, data: metric.data }]"
|
||||||
<VueApexCharts
|
class="chart absolute bottom-0 left-0 right-0 w-full opacity-0"
|
||||||
v-if="metric.showGraph && !loading"
|
/>
|
||||||
type="area"
|
</ClientOnly>
|
||||||
height="142"
|
|
||||||
:options="getChartOptions(metric.warning, index)"
|
|
||||||
:series="[{ name: metric.title, data: metric.data }]"
|
|
||||||
class="chart"
|
|
||||||
:class="chartsReady.has(index) ? 'opacity-100' : 'opacity-0'"
|
|
||||||
/>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component
|
<NuxtLink
|
||||||
:is="loading ? 'div' : 'NuxtLink'"
|
:to="`/servers/manage/${serverId}/files`"
|
||||||
:to="loading ? undefined : `/servers/manage/${serverId}/files`"
|
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100"
|
||||||
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
|
||||||
:class="loading ? '' : 'transition-transform duration-100 hover:scale-105 active:scale-100'"
|
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">
|
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">
|
||||||
{{ loading ? "0 B" : formatBytes(stats.storage_usage_bytes) }}
|
{{ formatBytes(stats.storage_usage_bytes) }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
||||||
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
||||||
</component>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, shallowRef } from "vue";
|
import { ref, computed, shallowRef } from "vue";
|
||||||
import { FolderOpenIcon, CPUIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
|
import { FolderOpenIcon, CPUIcon, DBIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
import type { Stats } from "@modrinth/utils";
|
import type { Stats } from "~/types/servers";
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
const serverId = route.params.id;
|
const serverId = route.params.id;
|
||||||
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
||||||
|
|
||||||
const chartsReady = ref(new Set<number>());
|
|
||||||
|
|
||||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||||
ramAsNumber: false,
|
ramAsNumber: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ data?: Stats; loading?: boolean }>(), {
|
const props = defineProps<{ data: Stats }>();
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stats = shallowRef(
|
const stats = shallowRef(props.data.current);
|
||||||
props.data?.current || {
|
|
||||||
cpu_percent: 0,
|
|
||||||
ram_usage_bytes: 0,
|
|
||||||
ram_total_bytes: 1, // Avoid division by zero
|
|
||||||
storage_usage_bytes: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChartReady = (index: number) => {
|
|
||||||
chartsReady.value.add(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
const formatBytes = (bytes: number) => {
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
@@ -121,29 +94,6 @@ const updateGraphData = (arr: number[], newValue: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const metrics = computed(() => {
|
const metrics = computed(() => {
|
||||||
if (props.loading) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: "CPU usage",
|
|
||||||
value: "0.00%",
|
|
||||||
max: "100%",
|
|
||||||
icon: CPUIcon,
|
|
||||||
data: cpuData.value,
|
|
||||||
showGraph: false,
|
|
||||||
warning: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Memory usage",
|
|
||||||
value: "0.00%",
|
|
||||||
max: "100%",
|
|
||||||
icon: DatabaseIcon,
|
|
||||||
data: ramData.value,
|
|
||||||
showGraph: false,
|
|
||||||
warning: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ramPercent = Math.min(
|
const ramPercent = Math.min(
|
||||||
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
||||||
100,
|
100,
|
||||||
@@ -169,7 +119,7 @@ const metrics = computed(() => {
|
|||||||
? formatBytes(stats.value.ram_usage_bytes)
|
? formatBytes(stats.value.ram_usage_bytes)
|
||||||
: `${ramPercent.toFixed(2)}%`,
|
: `${ramPercent.toFixed(2)}%`,
|
||||||
max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%",
|
max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%",
|
||||||
icon: DatabaseIcon,
|
icon: DBIcon,
|
||||||
data: ramData.value,
|
data: ramData.value,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
warning: ramPercent >= 90 ? "Memory usage is very high" : null,
|
warning: ramPercent >= 90 ? "Memory usage is very high" : null,
|
||||||
@@ -177,7 +127,7 @@ const metrics = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const getChartOptions = (hasWarning: string | null, index: number) => ({
|
const getChartOptions = (hasWarning: string | null) => ({
|
||||||
chart: {
|
chart: {
|
||||||
type: "area",
|
type: "area",
|
||||||
animations: { enabled: false },
|
animations: { enabled: false },
|
||||||
@@ -189,10 +139,6 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
|||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
events: {
|
|
||||||
mounted: () => onChartReady(index),
|
|
||||||
updated: () => onChartReady(index),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
stroke: { curve: "smooth", width: 3 },
|
stroke: { curve: "smooth", width: 3 },
|
||||||
fill: {
|
fill: {
|
||||||
@@ -226,26 +172,24 @@ const getChartOptions = (hasWarning: string | null, index: number) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.data?.current,
|
() => props.data.current,
|
||||||
(newStats) => {
|
(newStats) => {
|
||||||
if (newStats) {
|
stats.value = newStats;
|
||||||
stats.value = newStats;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-space {
|
.chart {
|
||||||
height: 142px;
|
animation: fadeIn 0.2s ease-out 0.2s forwards;
|
||||||
width: calc(100% + 48px);
|
|
||||||
margin-left: -24px;
|
margin-left: -24px;
|
||||||
margin-right: -24px;
|
margin-right: -24px;
|
||||||
|
width: calc(100% + 48px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart {
|
@keyframes fadeIn {
|
||||||
width: 100% !important;
|
to {
|
||||||
height: 142px !important;
|
opacity: 1;
|
||||||
transition: opacity 0.3s ease-out;
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LoaderIcon } from "@modrinth/assets";
|
import { LoaderIcon } from "@modrinth/assets";
|
||||||
import type { Loaders } from "@modrinth/utils";
|
import type { Loaders } from "~/types/servers";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
loader: Loaders;
|
loader: Loaders;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
|
import { RightArrowIcon, SparklesIcon, UnknownIcon } from "@modrinth/assets";
|
||||||
import type { MessageDescriptor } from "@vintl/vintl";
|
import type { MessageDescriptor } from "@vintl/vintl";
|
||||||
import { formatPrice } from "@modrinth/utils";
|
|
||||||
|
|
||||||
const { formatMessage, locale } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "select" | "scroll-to-faq"): void;
|
(e: "select" | "scroll-to-faq"): void;
|
||||||
@@ -18,8 +18,8 @@ const plans: Record<
|
|||||||
accentText: string;
|
accentText: string;
|
||||||
accentBg: string;
|
accentBg: string;
|
||||||
name: MessageDescriptor;
|
name: MessageDescriptor;
|
||||||
|
symbol: MessageDescriptor;
|
||||||
description: MessageDescriptor;
|
description: MessageDescriptor;
|
||||||
mostPopular: boolean;
|
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
small: {
|
small: {
|
||||||
@@ -30,11 +30,15 @@ const plans: Record<
|
|||||||
id: "servers.plan.small.name",
|
id: "servers.plan.small.name",
|
||||||
defaultMessage: "Small",
|
defaultMessage: "Small",
|
||||||
}),
|
}),
|
||||||
|
symbol: defineMessage({
|
||||||
|
id: "servers.plan.small.symbol",
|
||||||
|
defaultMessage: "S",
|
||||||
|
}),
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.small.description",
|
id: "servers.plan.small.description",
|
||||||
defaultMessage: "Perfect for 1–5 friends with a few light mods.",
|
defaultMessage:
|
||||||
|
"Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding.",
|
||||||
}),
|
}),
|
||||||
mostPopular: false,
|
|
||||||
},
|
},
|
||||||
medium: {
|
medium: {
|
||||||
buttonColor: "green",
|
buttonColor: "green",
|
||||||
@@ -44,11 +48,14 @@ const plans: Record<
|
|||||||
id: "servers.plan.medium.name",
|
id: "servers.plan.medium.name",
|
||||||
defaultMessage: "Medium",
|
defaultMessage: "Medium",
|
||||||
}),
|
}),
|
||||||
|
symbol: defineMessage({
|
||||||
|
id: "servers.plan.medium.symbol",
|
||||||
|
defaultMessage: "M",
|
||||||
|
}),
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.medium.description",
|
id: "servers.plan.medium.description",
|
||||||
defaultMessage: "Great for 6–15 players and multiple mods.",
|
defaultMessage: "Great for modded multiplayer and small communities.",
|
||||||
}),
|
}),
|
||||||
mostPopular: true,
|
|
||||||
},
|
},
|
||||||
large: {
|
large: {
|
||||||
buttonColor: "purple",
|
buttonColor: "purple",
|
||||||
@@ -58,11 +65,14 @@ const plans: Record<
|
|||||||
id: "servers.plan.large.name",
|
id: "servers.plan.large.name",
|
||||||
defaultMessage: "Large",
|
defaultMessage: "Large",
|
||||||
}),
|
}),
|
||||||
|
symbol: defineMessage({
|
||||||
|
id: "servers.plan.large.symbol",
|
||||||
|
defaultMessage: "L",
|
||||||
|
}),
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.large.description",
|
id: "servers.plan.large.description",
|
||||||
defaultMessage: "Ideal for 15–25 players, modpacks, or heavy modding.",
|
defaultMessage: "Ideal for larger communities, modpacks, and heavy modding.",
|
||||||
}),
|
}),
|
||||||
mostPopular: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,30 +83,42 @@ const props = defineProps<{
|
|||||||
storage: number;
|
storage: number;
|
||||||
cpus: number;
|
cpus: number;
|
||||||
price: number;
|
price: number;
|
||||||
interval: "monthly" | "quarterly" | "yearly";
|
|
||||||
currency: string;
|
|
||||||
isUsa: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const outOfStock = computed(() => {
|
const outOfStock = computed(() => {
|
||||||
return !props.capacity || props.capacity === 0;
|
return !props.capacity || props.capacity === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const billingMonths = computed(() => {
|
const lowStock = computed(() => {
|
||||||
if (props.interval === "yearly") {
|
return !props.capacity || props.capacity < 8;
|
||||||
return 12;
|
});
|
||||||
} else if (props.interval === "quarterly") {
|
|
||||||
return 3;
|
const formattedRam = computed(() => {
|
||||||
}
|
return props.ram / 1024;
|
||||||
return 1;
|
});
|
||||||
|
|
||||||
|
const formattedStorage = computed(() => {
|
||||||
|
return props.storage / 1024;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sharedCpus = computed(() => {
|
||||||
|
return props.cpus / 2;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="relative flex w-full flex-col justify-between">
|
<li class="relative flex w-full flex-col justify-between pt-12 lg:w-1/3">
|
||||||
|
<div
|
||||||
|
v-if="lowStock"
|
||||||
|
class="absolute left-0 right-0 top-[-2px] rounded-t-2xl p-4 text-center font-bold"
|
||||||
|
:class="outOfStock ? 'bg-bg-red' : 'bg-bg-orange'"
|
||||||
|
>
|
||||||
|
<template v-if="outOfStock"> Out of stock! </template>
|
||||||
|
<template v-else> Only {{ capacity }} left in stock! </template>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
:style="
|
:style="
|
||||||
plans[plan].mostPopular
|
plan === 'medium'
|
||||||
? {
|
? {
|
||||||
background: `radial-gradient(
|
background: `radial-gradient(
|
||||||
86.12% 101.64% at 95.97% 94.07%,
|
86.12% 101.64% at 95.97% 94.07%,
|
||||||
@@ -109,41 +131,55 @@ const billingMonths = computed(() => {
|
|||||||
: undefined
|
: undefined
|
||||||
"
|
"
|
||||||
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left"
|
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left"
|
||||||
|
:class="{ '!rounded-t-none': lowStock }"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-row flex-wrap items-center gap-3">
|
<div class="flex flex-row items-center justify-between">
|
||||||
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
|
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
|
||||||
<div
|
<div
|
||||||
v-if="plans[plan].mostPopular"
|
class="grid size-8 place-content-center rounded-full text-xs font-bold"
|
||||||
class="rounded-full bg-brand-highlight px-2 py-1 text-xs font-bold text-brand"
|
:class="`${plans[plan].accentBg} ${plans[plan].accentText}`"
|
||||||
>
|
>
|
||||||
Most popular
|
{{ formatMessage(plans[plan].symbol) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="m-0">{{ formatMessage(plans[plan].description) }}</p>
|
||||||
|
<div
|
||||||
|
class="flex flex-row flex-wrap items-center gap-2 text-nowrap text-secondary xl:justify-between"
|
||||||
|
>
|
||||||
|
<p class="m-0">{{ formattedRam }} GB RAM</p>
|
||||||
|
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||||
|
<p class="m-0">{{ formattedStorage }} GB SSD</p>
|
||||||
|
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
||||||
|
<p class="m-0">{{ sharedCpus }} Shared CPUs</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-secondary">
|
||||||
|
<SparklesIcon /> Bursts up to {{ cpus }} CPUs
|
||||||
|
<nuxt-link
|
||||||
|
v-tooltip="
|
||||||
|
`CPU bursting allows your server to temporarily use additional threads to help mitigate TPS spikes. Click for more info.`
|
||||||
|
"
|
||||||
|
to="/servers#cpu-burst"
|
||||||
|
@click="() => emit('scroll-to-faq')"
|
||||||
|
>
|
||||||
|
<UnknownIcon class="h-4 w-4 text-secondary opacity-80" />
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
<span class="m-0 text-2xl font-bold text-contrast">
|
<span class="m-0 text-2xl font-bold text-contrast">
|
||||||
{{ formatPrice(locale, price / billingMonths, currency, true) }}
|
${{ price / 100 }}<span class="text-lg font-semibold text-secondary">/month</span>
|
||||||
{{ isUsa ? "" : currency }}
|
|
||||||
<span class="text-lg font-semibold text-secondary">
|
|
||||||
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<p class="m-0 max-w-[18rem]">{{ formatMessage(plans[plan].description) }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
:color="plans[plan].buttonColor"
|
:color="plans[plan].buttonColor"
|
||||||
:type="plans[plan].mostPopular ? 'standard' : 'highlight-colored-text'"
|
:type="plan === 'medium' ? 'standard' : 'highlight-colored-text'"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<span v-if="outOfStock" class="button-like disabled"> Out of Stock </span>
|
<span v-if="outOfStock" class="button-like disabled"> Out of Stock </span>
|
||||||
<button v-else @click="() => emit('select')">Select plan</button>
|
<button v-else @click="() => emit('select')">
|
||||||
|
Get Started
|
||||||
|
<RightArrowIcon class="shrink-0" />
|
||||||
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ServersSpecs
|
|
||||||
:ram="ram"
|
|
||||||
:storage="storage"
|
|
||||||
:cpus="cpus"
|
|
||||||
:bursting-link="'/servers#cpu-burst'"
|
|
||||||
@click-bursting-link="() => emit('scroll-to-faq')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modri
|
|||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
||||||
|
|
||||||
const app = useNuxtApp() as unknown as { $notify: any };
|
const app = useNuxtApp() as unknown as { $notify: any };
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === "no
|
|||||||
const inputField = ref("");
|
const inputField = ref("");
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
await useServersFetch("notices").then((res) => {
|
await usePyroFetch("notices").then((res) => {
|
||||||
const notices = res as ServerNoticeType[];
|
const notices = res as ServerNoticeType[];
|
||||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
|
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
|
||||||
});
|
});
|
||||||
@@ -33,12 +33,9 @@ async function assign(server: boolean = true) {
|
|||||||
const input = inputField.value.trim();
|
const input = inputField.value.trim();
|
||||||
|
|
||||||
if (input !== "" && notice.value) {
|
if (input !== "" && notice.value) {
|
||||||
await useServersFetch(
|
await usePyroFetch(`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`, {
|
||||||
`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`,
|
method: "PUT",
|
||||||
{
|
}).catch((err) => {
|
||||||
method: "PUT",
|
|
||||||
},
|
|
||||||
).catch((err) => {
|
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: "main",
|
||||||
title: "Error assigning notice",
|
title: "Error assigning notice",
|
||||||
@@ -78,12 +75,9 @@ async function unassignDetect() {
|
|||||||
|
|
||||||
async function unassign(id: string, server: boolean = true) {
|
async function unassign(id: string, server: boolean = true) {
|
||||||
if (notice.value) {
|
if (notice.value) {
|
||||||
await useServersFetch(
|
await usePyroFetch(`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`, {
|
||||||
`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`,
|
method: "PUT",
|
||||||
{
|
}).catch((err) => {
|
||||||
method: "PUT",
|
|
||||||
},
|
|
||||||
).catch((err) => {
|
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: "main",
|
||||||
title: "Error unassigning notice",
|
title: "Error unassigning notice",
|
||||||
|
|||||||
@@ -2,8 +2,13 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
|
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
|
||||||
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
|
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
|
||||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||||
import { useRelativeTime, getDismissableMetadata, NOTICE_LEVELS } from "@modrinth/ui";
|
import { useRelativeTime } from "@modrinth/ui";
|
||||||
|
import {
|
||||||
|
DISMISSABLE,
|
||||||
|
getDismissableMetadata,
|
||||||
|
NOTICE_LEVELS,
|
||||||
|
} from "@modrinth/ui/src/utils/notices.ts";
|
||||||
import { useVIntl } from "@vintl/vintl";
|
import { useVIntl } from "@vintl/vintl";
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { CopyCode, OverflowMenu, MarkdownEditor } from "@modrinth/ui";
|
import { OverflowMenu, MarkdownEditor } from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
ReplyIcon,
|
ReplyIcon,
|
||||||
@@ -226,6 +226,7 @@ import {
|
|||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { useImageUpload } from "~/composables/image-upload.ts";
|
import { useImageUpload } from "~/composables/image-upload.ts";
|
||||||
|
import CopyCode from "~/components/ui/CopyCode.vue";
|
||||||
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
||||||
import { isStaff } from "~/helpers/users.js";
|
import { isStaff } from "~/helpers/users.js";
|
||||||
import { isApproved, isRejected } from "~/helpers/projects.js";
|
import { isApproved, isRejected } from "~/helpers/projects.js";
|
||||||
|
|||||||
@@ -103,8 +103,10 @@ import {
|
|||||||
ModrinthIcon,
|
ModrinthIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
import { AutoLink, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
||||||
import { renderString } from "@modrinth/utils";
|
import { renderString } from "@modrinth/utils";
|
||||||
|
import Avatar from "~/components/ui/Avatar.vue";
|
||||||
|
import Badge from "~/components/ui/Badge.vue";
|
||||||
import { isStaff } from "~/helpers/users.js";
|
import { isStaff } from "~/helpers/users.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export const useUserCountry = () => {
|
|||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (fromServer.value) return;
|
if (fromServer.value) return;
|
||||||
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
|
||||||
const lang = navigator.language || navigator.userLanguage || "";
|
const lang = navigator.language || navigator.userLanguage || "";
|
||||||
const region = lang.split("-")[1];
|
const region = lang.split("-")[1];
|
||||||
if (region) {
|
if (region) {
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
|||||||
projectBackground: false,
|
projectBackground: false,
|
||||||
searchBackground: false,
|
searchBackground: false,
|
||||||
advancedDebugInfo: false,
|
advancedDebugInfo: false,
|
||||||
showProjectPageDownloadModalServersPromo: true,
|
|
||||||
showProjectPageCreateServersTooltip: true,
|
|
||||||
showProjectPageQuickServerButton: false,
|
|
||||||
// advancedRendering: true,
|
// advancedRendering: true,
|
||||||
// externalLinksNewTab: true,
|
// externalLinksNewTab: true,
|
||||||
// notUsingBlockers: false,
|
// notUsingBlockers: false,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
|
|||||||
|
|
||||||
// Make sure file is less than 1MB
|
// Make sure file is less than 1MB
|
||||||
if (file.size > 1024 * 1024) {
|
if (file.size > 1024 * 1024) {
|
||||||
throw new Error("File exceeds the 1MiB size limit");
|
throw new Error("File is too large");
|
||||||
}
|
}
|
||||||
|
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { $fetch, FetchError } from "ofetch";
|
||||||
|
|
||||||
|
interface PyroFetchOptions {
|
||||||
|
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
contentType?: string;
|
||||||
|
body?: Record<string, any>;
|
||||||
|
version?: number;
|
||||||
|
override?: {
|
||||||
|
url?: string;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
retry?: boolean;
|
||||||
|
bypassAuth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PyroFetchError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode?: number,
|
||||||
|
public originalError?: Error,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "PyroFetchError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function usePyroFetch<T>(path: string, options: PyroFetchOptions = {}): Promise<T> {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const auth = await useAuth();
|
||||||
|
const authToken = auth.value?.token;
|
||||||
|
|
||||||
|
if (!authToken && !options.bypassAuth) {
|
||||||
|
throw new PyroFetchError("Cannot pyrofetch without auth", 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { method = "GET", contentType = "application/json", body, version = 0, override } = options;
|
||||||
|
|
||||||
|
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
||||||
|
/\/$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!base) {
|
||||||
|
throw new PyroFetchError(
|
||||||
|
"Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
|
||||||
|
10001,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = override?.url
|
||||||
|
? `https://${override.url}/${path.replace(/^\//, "")}`
|
||||||
|
: `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`;
|
||||||
|
|
||||||
|
type HeadersRecord = Record<string, string>;
|
||||||
|
|
||||||
|
const authHeader: HeadersRecord = options.bypassAuth
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
Authorization: `Bearer ${override?.token ?? authToken}`,
|
||||||
|
"Access-Control-Allow-Headers": "Authorization",
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers: HeadersRecord = {
|
||||||
|
...authHeader,
|
||||||
|
"User-Agent": "Pyro/1.0 (https://pyro.host)",
|
||||||
|
Vary: "Accept, Origin",
|
||||||
|
"Content-Type": contentType,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (import.meta.client && typeof window !== "undefined") {
|
||||||
|
headers.Origin = window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await $fetch<T>(fullUrl, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
||||||
|
timeout: 10000,
|
||||||
|
retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fetch error:", error);
|
||||||
|
if (error instanceof FetchError) {
|
||||||
|
const statusCode = error.response?.status;
|
||||||
|
const statusText = error.response?.statusText || "Unknown error";
|
||||||
|
const errorMessages: { [key: number]: string } = {
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Not Found",
|
||||||
|
405: "Method Not Allowed",
|
||||||
|
429: "Too Many Requests",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
502: "Bad Gateway",
|
||||||
|
};
|
||||||
|
const message =
|
||||||
|
statusCode && statusCode in errorMessages
|
||||||
|
? errorMessages[statusCode]
|
||||||
|
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
||||||
|
throw new PyroFetchError(`[PYROFETCH][PYRO] ${message}`, statusCode, error);
|
||||||
|
}
|
||||||
|
throw new PyroFetchError(
|
||||||
|
"[PYROFETCH][PYRO] An unexpected error occurred during the fetch operation.",
|
||||||
|
undefined,
|
||||||
|
error as Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,282 +0,0 @@
|
|||||||
import { ModrinthServerError } from "@modrinth/utils";
|
|
||||||
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "./servers-fetch.ts";
|
|
||||||
|
|
||||||
import {
|
|
||||||
GeneralModule,
|
|
||||||
ContentModule,
|
|
||||||
BackupsModule,
|
|
||||||
NetworkModule,
|
|
||||||
StartupModule,
|
|
||||||
WSModule,
|
|
||||||
FSModule,
|
|
||||||
} from "./modules/index.ts";
|
|
||||||
|
|
||||||
export function handleError(err: any) {
|
|
||||||
if (err instanceof ModrinthServerError && err.v1Error) {
|
|
||||||
addNotification({
|
|
||||||
title: err.v1Error?.context ?? `An error occurred`,
|
|
||||||
type: "error",
|
|
||||||
text: err.v1Error.description,
|
|
||||||
errorCode: err.v1Error.error,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
addNotification({
|
|
||||||
title: "An error occurred",
|
|
||||||
type: "error",
|
|
||||||
text: err.message ?? (err.data ? err.data.description : err),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ModrinthServer {
|
|
||||||
readonly serverId: string;
|
|
||||||
private errors: Partial<Record<ModuleName, ModuleError>> = {};
|
|
||||||
|
|
||||||
readonly general: GeneralModule;
|
|
||||||
readonly content: ContentModule;
|
|
||||||
readonly backups: BackupsModule;
|
|
||||||
readonly network: NetworkModule;
|
|
||||||
readonly startup: StartupModule;
|
|
||||||
readonly ws: WSModule;
|
|
||||||
readonly fs: FSModule;
|
|
||||||
|
|
||||||
constructor(serverId: string) {
|
|
||||||
this.serverId = serverId;
|
|
||||||
|
|
||||||
this.general = new GeneralModule(this);
|
|
||||||
this.content = new ContentModule(this);
|
|
||||||
this.backups = new BackupsModule(this);
|
|
||||||
this.network = new NetworkModule(this);
|
|
||||||
this.startup = new StartupModule(this);
|
|
||||||
this.ws = new WSModule(this);
|
|
||||||
this.fs = new FSModule(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createMissingFolders(path: string): Promise<void> {
|
|
||||||
if (path.startsWith("/")) {
|
|
||||||
path = path.substring(1);
|
|
||||||
}
|
|
||||||
const folders = path.split("/");
|
|
||||||
let currentPath = "";
|
|
||||||
|
|
||||||
for (const folder of folders) {
|
|
||||||
currentPath += "/" + folder;
|
|
||||||
try {
|
|
||||||
await this.fs.createFileOrFolder(currentPath, "directory");
|
|
||||||
} catch {
|
|
||||||
// Folder might already exist, ignore error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchConfigFile(fileName: string): Promise<any> {
|
|
||||||
return await useServersFetch(`servers/${this.serverId}/config/${fileName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructServerProperties(properties: any): string {
|
|
||||||
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(properties)) {
|
|
||||||
if (typeof value === "object") {
|
|
||||||
fileContent += `${key}=${JSON.stringify(value)}\n`;
|
|
||||||
} else if (typeof value === "boolean") {
|
|
||||||
fileContent += `${key}=${value ? "true" : "false"}\n`;
|
|
||||||
} else {
|
|
||||||
fileContent += `${key}=${value}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
|
|
||||||
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`);
|
|
||||||
|
|
||||||
if (sharedImage.value) {
|
|
||||||
return sharedImage.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
|
||||||
try {
|
|
||||||
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
|
|
||||||
override: auth,
|
|
||||||
retry: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fileData instanceof Blob && import.meta.client) {
|
|
||||||
const dataURL = await new Promise<string>((resolve) => {
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
canvas.width = 512;
|
|
||||||
canvas.height = 512;
|
|
||||||
ctx?.drawImage(img, 0, 0, 512, 512);
|
|
||||||
const dataURL = canvas.toDataURL("image/png");
|
|
||||||
sharedImage.value = dataURL;
|
|
||||||
resolve(dataURL);
|
|
||||||
URL.revokeObjectURL(img.src);
|
|
||||||
};
|
|
||||||
img.src = URL.createObjectURL(fileData);
|
|
||||||
});
|
|
||||||
return dataURL;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ModrinthServerError && error.statusCode === 404) {
|
|
||||||
if (iconUrl) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(iconUrl);
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch icon");
|
|
||||||
const file = await response.blob();
|
|
||||||
const originalFile = new File([file], "server-icon-original.png", {
|
|
||||||
type: "image/png",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (import.meta.client) {
|
|
||||||
const dataURL = await new Promise<string>((resolve) => {
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
canvas.width = 64;
|
|
||||||
canvas.height = 64;
|
|
||||||
ctx?.drawImage(img, 0, 0, 64, 64);
|
|
||||||
canvas.toBlob(async (blob) => {
|
|
||||||
if (blob) {
|
|
||||||
const scaledFile = new File([blob], "server-icon.png", {
|
|
||||||
type: "image/png",
|
|
||||||
});
|
|
||||||
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
|
|
||||||
method: "POST",
|
|
||||||
contentType: "application/octet-stream",
|
|
||||||
body: scaledFile,
|
|
||||||
override: auth,
|
|
||||||
});
|
|
||||||
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
|
|
||||||
method: "POST",
|
|
||||||
contentType: "application/octet-stream",
|
|
||||||
body: originalFile,
|
|
||||||
override: auth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, "image/png");
|
|
||||||
const dataURL = canvas.toDataURL("image/png");
|
|
||||||
sharedImage.value = dataURL;
|
|
||||||
resolve(dataURL);
|
|
||||||
URL.revokeObjectURL(img.src);
|
|
||||||
};
|
|
||||||
img.src = URL.createObjectURL(file);
|
|
||||||
});
|
|
||||||
return dataURL;
|
|
||||||
}
|
|
||||||
} catch (externalError: any) {
|
|
||||||
console.debug("Could not process external icon:", externalError.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.debug("Icon processing failed:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
sharedImage.value = undefined;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async refresh(
|
|
||||||
modules: ModuleName[] = [],
|
|
||||||
options?: {
|
|
||||||
preserveConnection?: boolean;
|
|
||||||
preserveInstallState?: boolean;
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
|
||||||
const modulesToRefresh =
|
|
||||||
modules.length > 0
|
|
||||||
? modules
|
|
||||||
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
|
|
||||||
|
|
||||||
for (const module of modulesToRefresh) {
|
|
||||||
try {
|
|
||||||
switch (module) {
|
|
||||||
case "general": {
|
|
||||||
if (options?.preserveConnection) {
|
|
||||||
const currentImage = this.general.image;
|
|
||||||
const currentMotd = this.general.motd;
|
|
||||||
const currentStatus = this.general.status;
|
|
||||||
|
|
||||||
await this.general.fetch();
|
|
||||||
|
|
||||||
if (currentImage) {
|
|
||||||
this.general.image = currentImage;
|
|
||||||
}
|
|
||||||
if (currentMotd) {
|
|
||||||
this.general.motd = currentMotd;
|
|
||||||
}
|
|
||||||
if (options.preserveInstallState && currentStatus === "installing") {
|
|
||||||
this.general.status = "installing";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.general.fetch();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "content":
|
|
||||||
await this.content.fetch();
|
|
||||||
break;
|
|
||||||
case "backups":
|
|
||||||
await this.backups.fetch();
|
|
||||||
break;
|
|
||||||
case "network":
|
|
||||||
await this.network.fetch();
|
|
||||||
break;
|
|
||||||
case "startup":
|
|
||||||
await this.startup.fetch();
|
|
||||||
break;
|
|
||||||
case "ws":
|
|
||||||
await this.ws.fetch();
|
|
||||||
break;
|
|
||||||
case "fs":
|
|
||||||
await this.fs.fetch();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ModrinthServerError) {
|
|
||||||
if (error.statusCode === 404 && ["fs", "content"].includes(module)) {
|
|
||||||
console.debug(`Optional ${module} resource not found:`, error.message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.statusCode === 503) {
|
|
||||||
console.debug(`Temporary ${module} unavailable:`, error.message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.errors[module] = {
|
|
||||||
error:
|
|
||||||
error instanceof ModrinthServerError
|
|
||||||
? error
|
|
||||||
: new ModrinthServerError("Unknown error", undefined, error as Error),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get moduleErrors() {
|
|
||||||
return this.errors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useModrinthServers = async (
|
|
||||||
serverId: string,
|
|
||||||
includedModules: ModuleName[] = ["general"],
|
|
||||||
) => {
|
|
||||||
const server = new ModrinthServer(serverId);
|
|
||||||
await server.refresh(includedModules);
|
|
||||||
return reactive(server);
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { Backup, AutoBackupSettings } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "../servers-fetch.ts";
|
|
||||||
import { ServerModule } from "./base.ts";
|
|
||||||
|
|
||||||
export class BackupsModule extends ServerModule {
|
|
||||||
data: Backup[] = [];
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
this.data = await useServersFetch<Backup[]>(`servers/${this.serverId}/backups`, {}, "backups");
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(backupName: string): Promise<string> {
|
|
||||||
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { name: backupName },
|
|
||||||
});
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
return response.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async rename(backupId: string, newName: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { name: newName },
|
|
||||||
});
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(backupId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
}
|
|
||||||
|
|
||||||
async restore(backupId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
}
|
|
||||||
|
|
||||||
async prepare(backupId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/prepare-download`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async lock(backupId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
}
|
|
||||||
|
|
||||||
async unlock(backupId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
}
|
|
||||||
|
|
||||||
async retry(backupId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateAutoBackup(autoBackup: "enable" | "disable", interval: number): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/autobackup`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { set: autoBackup, interval },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAutoBackup(): Promise<AutoBackupSettings> {
|
|
||||||
return await useServersFetch(`servers/${this.serverId}/autobackup`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { ModrinthServer } from "../modrinth-servers.ts";
|
|
||||||
|
|
||||||
export abstract class ServerModule {
|
|
||||||
protected server: ModrinthServer;
|
|
||||||
|
|
||||||
constructor(server: ModrinthServer) {
|
|
||||||
this.server = server;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get serverId(): string {
|
|
||||||
return this.server.serverId;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fetch(): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import type { Mod, ContentType } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "../servers-fetch.ts";
|
|
||||||
import { ServerModule } from "./base.ts";
|
|
||||||
|
|
||||||
export class ContentModule extends ServerModule {
|
|
||||||
data: Mod[] = [];
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
const mods = await useServersFetch<Mod[]>(`servers/${this.serverId}/mods`, {}, "content");
|
|
||||||
this.data = mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
async install(contentType: ContentType, projectId: string, versionId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/mods`, {
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
rinth_ids: { project_id: projectId, version_id: versionId },
|
|
||||||
install_as: contentType,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(path: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/deleteMod`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { path },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async reinstall(replace: string, projectId: string, versionId: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/mods/update`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { replace, project_id: projectId, version_id: versionId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import type {
|
|
||||||
FileUploadQuery,
|
|
||||||
JWTAuth,
|
|
||||||
DirectoryResponse,
|
|
||||||
FilesystemOp,
|
|
||||||
FSQueuedOp,
|
|
||||||
} from "@modrinth/utils";
|
|
||||||
import { ModrinthServerError } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "../servers-fetch.ts";
|
|
||||||
import { ServerModule } from "./base.ts";
|
|
||||||
|
|
||||||
export class FSModule extends ServerModule {
|
|
||||||
auth!: JWTAuth;
|
|
||||||
ops: FilesystemOp[] = [];
|
|
||||||
queuedOps: FSQueuedOp[] = [];
|
|
||||||
opsQueuedForModification: string[] = [];
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
this.auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`, {}, "fs");
|
|
||||||
this.ops = [];
|
|
||||||
this.queuedOps = [];
|
|
||||||
this.opsQueuedForModification = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async retryWithAuth<T>(requestFn: () => Promise<T>): Promise<T> {
|
|
||||||
try {
|
|
||||||
return await requestFn();
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ModrinthServerError && error.statusCode === 401) {
|
|
||||||
await this.fetch(); // Refresh auth
|
|
||||||
return await requestFn();
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listDirContents(path: string, page: number, pageSize: number): Promise<DirectoryResponse> {
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
const encodedPath = encodeURIComponent(path);
|
|
||||||
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
|
|
||||||
override: this.auth,
|
|
||||||
retry: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
const encodedPath = encodeURIComponent(path);
|
|
||||||
await useServersFetch(`/create?path=${encodedPath}&type=${type}`, {
|
|
||||||
method: "POST",
|
|
||||||
contentType: "application/octet-stream",
|
|
||||||
override: this.auth,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadFile(path: string, file: File): FileUploadQuery {
|
|
||||||
const encodedPath = encodeURIComponent(path);
|
|
||||||
const progressSubject = new EventTarget();
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
const uploadPromise = new Promise((resolve, reject) => {
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhr.upload.addEventListener("progress", (e) => {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
const progress = (e.loaded / e.total) * 100;
|
|
||||||
progressSubject.dispatchEvent(
|
|
||||||
new CustomEvent("progress", {
|
|
||||||
detail: { loaded: e.loaded, total: e.total, progress },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
|
||||||
if (xhr.status >= 200 && xhr.status < 300) {
|
|
||||||
resolve(xhr.response);
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
||||||
xhr.onabort = () => reject(new Error("Upload cancelled"));
|
|
||||||
|
|
||||||
xhr.open("POST", `https://${this.auth.url}/create?path=${encodedPath}&type=file`);
|
|
||||||
xhr.setRequestHeader("Authorization", `Bearer ${this.auth.token}`);
|
|
||||||
xhr.setRequestHeader("Content-Type", "application/octet-stream");
|
|
||||||
xhr.send(file);
|
|
||||||
|
|
||||||
abortController.signal.addEventListener("abort", () => xhr.abort());
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
promise: uploadPromise,
|
|
||||||
onProgress: (
|
|
||||||
callback: (progress: { loaded: number; total: number; progress: number }) => void,
|
|
||||||
) => {
|
|
||||||
progressSubject.addEventListener("progress", ((e: CustomEvent) => {
|
|
||||||
callback(e.detail);
|
|
||||||
}) as EventListener);
|
|
||||||
},
|
|
||||||
cancel: () => abortController.abort(),
|
|
||||||
} as FileUploadQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
renameFileOrFolder(path: string, name: string): Promise<void> {
|
|
||||||
const pathName = path.split("/").slice(0, -1).join("/") + "/" + name;
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
await useServersFetch(`/move`, {
|
|
||||||
method: "POST",
|
|
||||||
override: this.auth,
|
|
||||||
body: { source: path, destination: pathName },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFile(path: string, content: string): Promise<void> {
|
|
||||||
const octetStream = new Blob([content], { type: "application/octet-stream" });
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
await useServersFetch(`/update?path=${path}`, {
|
|
||||||
method: "PUT",
|
|
||||||
contentType: "application/octet-stream",
|
|
||||||
body: octetStream,
|
|
||||||
override: this.auth,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
moveFileOrFolder(path: string, newPath: string): Promise<void> {
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
await this.server.createMissingFolders(newPath.substring(0, newPath.lastIndexOf("/")));
|
|
||||||
await useServersFetch(`/move`, {
|
|
||||||
method: "POST",
|
|
||||||
override: this.auth,
|
|
||||||
body: { source: path, destination: newPath },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
|
|
||||||
const encodedPath = encodeURIComponent(path);
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
await useServersFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
override: this.auth,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadFile(path: string, raw?: boolean): Promise<any> {
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
const encodedPath = encodeURIComponent(path);
|
|
||||||
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
|
|
||||||
override: this.auth,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fileData instanceof Blob) {
|
|
||||||
return raw ? fileData : await fileData.text();
|
|
||||||
}
|
|
||||||
return fileData;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
extractFile(
|
|
||||||
path: string,
|
|
||||||
override = true,
|
|
||||||
dry = false,
|
|
||||||
silentQueue = false,
|
|
||||||
): Promise<{ modpack_name: string | null; conflicting_files: string[] }> {
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
const encodedPath = encodeURIComponent(path);
|
|
||||||
|
|
||||||
if (!silentQueue) {
|
|
||||||
this.queuedOps.push({ op: "unarchive", src: path });
|
|
||||||
setTimeout(() => this.removeQueuedOp("unarchive", path), 4000);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await useServersFetch(
|
|
||||||
`/unarchive?src=${encodedPath}&trg=/&override=${override}&dry=${dry}`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
override: this.auth,
|
|
||||||
version: 1,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
"Error extracting file",
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
this.removeQueuedOp("unarchive", path);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
modifyOp(id: string, action: "dismiss" | "cancel"): Promise<void> {
|
|
||||||
return this.retryWithAuth(async () => {
|
|
||||||
await useServersFetch(
|
|
||||||
`/ops/${action}?id=${id}`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
override: this.auth,
|
|
||||||
version: 1,
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
`Error ${action === "dismiss" ? "dismissing" : "cancelling"} filesystem operation`,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.opsQueuedForModification = this.opsQueuedForModification.filter((x: string) => x !== id);
|
|
||||||
this.ops = this.ops.filter((x: FilesystemOp) => x.id !== id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeQueuedOp(op: FSQueuedOp["op"], src: string): void {
|
|
||||||
this.queuedOps = this.queuedOps.filter((x: FSQueuedOp) => x.op !== op || x.src !== src);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearQueuedOps(): void {
|
|
||||||
this.queuedOps = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import { $fetch } from "ofetch";
|
|
||||||
import type { ServerGeneral, Project, PowerAction, JWTAuth } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "../servers-fetch.ts";
|
|
||||||
import { ServerModule } from "./base.ts";
|
|
||||||
|
|
||||||
export class GeneralModule extends ServerModule implements ServerGeneral {
|
|
||||||
server_id!: string;
|
|
||||||
name!: string;
|
|
||||||
net!: { ip: string; port: number; domain: string };
|
|
||||||
game!: string;
|
|
||||||
backup_quota!: number;
|
|
||||||
used_backup_quota!: number;
|
|
||||||
status!: string;
|
|
||||||
suspension_reason!: string;
|
|
||||||
loader!: string;
|
|
||||||
loader_version!: string;
|
|
||||||
mc_version!: string;
|
|
||||||
upstream!: {
|
|
||||||
kind: "modpack" | "mod" | "resourcepack";
|
|
||||||
version_id: string;
|
|
||||||
project_id: string;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
motd?: string;
|
|
||||||
image?: string;
|
|
||||||
project?: Project;
|
|
||||||
sftp_username!: string;
|
|
||||||
sftp_password!: string;
|
|
||||||
sftp_host!: string;
|
|
||||||
datacenter?: string;
|
|
||||||
notices?: any[];
|
|
||||||
node!: { token: string; instance: string };
|
|
||||||
flows?: { intro?: boolean };
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, "general");
|
|
||||||
|
|
||||||
if (data.upstream?.project_id) {
|
|
||||||
const project = await $fetch(
|
|
||||||
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
|
|
||||||
);
|
|
||||||
data.project = project as Project;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.client) {
|
|
||||||
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const motd = await this.getMotd();
|
|
||||||
if (motd === "A Minecraft Server") {
|
|
||||||
await this.setMotd(
|
|
||||||
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
data.motd = motd;
|
|
||||||
|
|
||||||
// Copy data to this module
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateName(newName: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/name`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { name: newName },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async power(action: PowerAction): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/power`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { action },
|
|
||||||
});
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
}
|
|
||||||
|
|
||||||
async reinstall(
|
|
||||||
loader: boolean,
|
|
||||||
projectId: string,
|
|
||||||
versionId?: string,
|
|
||||||
loaderVersionId?: string,
|
|
||||||
hardReset: boolean = false,
|
|
||||||
): Promise<void> {
|
|
||||||
const hardResetParam = hardReset ? "true" : "false";
|
|
||||||
if (loader) {
|
|
||||||
if (projectId.toLowerCase() === "neoforge") {
|
|
||||||
projectId = "NeoForge";
|
|
||||||
}
|
|
||||||
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { loader: projectId, loader_version: loaderVersionId, game_version: versionId },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { project_id: projectId, version_id: versionId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async reinstallFromMrpack(mrpack: File, hardReset: boolean = false): Promise<void> {
|
|
||||||
const hardResetParam = hardReset ? "true" : "false";
|
|
||||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", mrpack);
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${auth.token}`,
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
signal: AbortSignal.timeout(30 * 60 * 1000),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`[pyroservers] native fetch err status: ${response.status}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async suspend(status: boolean): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/suspend`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { suspended: status },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async endIntro(): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/flows/intro`, {
|
|
||||||
method: "DELETE",
|
|
||||||
version: 1,
|
|
||||||
});
|
|
||||||
await this.fetch(); // Refresh this module
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMotd(): Promise<string | undefined> {
|
|
||||||
try {
|
|
||||||
const props = await this.server.fs.downloadFile("/server.properties");
|
|
||||||
if (props) {
|
|
||||||
const lines = props.split("\n");
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("motd=")) {
|
|
||||||
return line.slice(5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setMotd(motd: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
|
|
||||||
if (props) {
|
|
||||||
props.motd = motd;
|
|
||||||
const newProps = this.server.constructServerProperties(props);
|
|
||||||
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
|
|
||||||
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
|
||||||
|
|
||||||
await useServersFetch(`/update?path=/server.properties`, {
|
|
||||||
method: "PUT",
|
|
||||||
contentType: "application/octet-stream",
|
|
||||||
body: octetStream,
|
|
||||||
override: auth,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
console.error(
|
|
||||||
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export * from "./base.ts";
|
|
||||||
export * from "./backups.ts";
|
|
||||||
export * from "./content.ts";
|
|
||||||
export * from "./fs.ts";
|
|
||||||
export * from "./general.ts";
|
|
||||||
export * from "./network.ts";
|
|
||||||
export * from "./startup.ts";
|
|
||||||
export * from "./ws.ts";
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import type { Allocation } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "../servers-fetch.ts";
|
|
||||||
import { ServerModule } from "./base.ts";
|
|
||||||
|
|
||||||
export class NetworkModule extends ServerModule {
|
|
||||||
allocations: Allocation[] = [];
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
this.allocations = await useServersFetch<Allocation[]>(
|
|
||||||
`servers/${this.serverId}/allocations`,
|
|
||||||
{},
|
|
||||||
"network",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async reserveAllocation(name: string): Promise<Allocation> {
|
|
||||||
return await useServersFetch<Allocation>(`servers/${this.serverId}/allocations?name=${name}`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateAllocation(port: number, name: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/allocations/${port}?name=${name}`, {
|
|
||||||
method: "PUT",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteAllocation(port: number): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/allocations/${port}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkSubdomainAvailability(subdomain: string): Promise<boolean> {
|
|
||||||
const result = (await useServersFetch(`subdomains/${subdomain}/isavailable`)) as {
|
|
||||||
available: boolean;
|
|
||||||
};
|
|
||||||
return result.available;
|
|
||||||
}
|
|
||||||
|
|
||||||
async changeSubdomain(subdomain: string): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/subdomain`, {
|
|
||||||
method: "POST",
|
|
||||||
body: { subdomain },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { Startup, JDKVersion, JDKBuild } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "../servers-fetch.ts";
|
|
||||||
import { ServerModule } from "./base.ts";
|
|
||||||
|
|
||||||
export class StartupModule extends ServerModule implements Startup {
|
|
||||||
invocation!: string;
|
|
||||||
original_invocation!: string;
|
|
||||||
jdk_version!: JDKVersion;
|
|
||||||
jdk_build!: JDKBuild;
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
const data = await useServersFetch<Startup>(`servers/${this.serverId}/startup`, {}, "startup");
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(invocation: string, jdkVersion: JDKVersion, jdkBuild: JDKBuild): Promise<void> {
|
|
||||||
await useServersFetch(`servers/${this.serverId}/startup`, {
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
invocation: invocation || null,
|
|
||||||
jdk_version: jdkVersion || null,
|
|
||||||
jdk_build: jdkBuild || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import type { JWTAuth } from "@modrinth/utils";
|
|
||||||
import { useServersFetch } from "../servers-fetch.ts";
|
|
||||||
import { ServerModule } from "./base.ts";
|
|
||||||
|
|
||||||
export class WSModule extends ServerModule implements JWTAuth {
|
|
||||||
url!: string;
|
|
||||||
token!: string;
|
|
||||||
|
|
||||||
async fetch(): Promise<void> {
|
|
||||||
const data = await useServersFetch<JWTAuth>(`servers/${this.serverId}/ws`, {}, "ws");
|
|
||||||
Object.assign(this, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import { $fetch, FetchError } from "ofetch";
|
|
||||||
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
|
|
||||||
import type { V1ErrorInfo } from "@modrinth/utils";
|
|
||||||
|
|
||||||
export interface ServersFetchOptions {
|
|
||||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
||||||
contentType?: string;
|
|
||||||
body?: Record<string, any>;
|
|
||||||
version?: number;
|
|
||||||
override?: {
|
|
||||||
url?: string;
|
|
||||||
token?: string;
|
|
||||||
};
|
|
||||||
retry?: number | boolean;
|
|
||||||
bypassAuth?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function useServersFetch<T>(
|
|
||||||
path: string,
|
|
||||||
options: ServersFetchOptions = {},
|
|
||||||
module?: string,
|
|
||||||
errorContext?: string,
|
|
||||||
): Promise<T> {
|
|
||||||
const config = useRuntimeConfig();
|
|
||||||
const auth = await useAuth();
|
|
||||||
const authToken = auth.value?.token;
|
|
||||||
|
|
||||||
if (!authToken && !options.bypassAuth) {
|
|
||||||
const error = new ModrinthServersFetchError(
|
|
||||||
"[Modrinth Servers] Cannot fetch without auth",
|
|
||||||
10000,
|
|
||||||
);
|
|
||||||
throw new ModrinthServerError("Missing auth token", 401, error, module);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
method = "GET",
|
|
||||||
contentType = "application/json",
|
|
||||||
body,
|
|
||||||
version = 0,
|
|
||||||
override,
|
|
||||||
retry = method === "GET" ? 3 : 0,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
|
||||||
/\/$/,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!base) {
|
|
||||||
const error = new ModrinthServersFetchError(
|
|
||||||
"[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
|
|
||||||
10001,
|
|
||||||
);
|
|
||||||
throw new ModrinthServerError("Configuration error: Missing PYRO_BASE_URL", 500, error, module);
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionString = `v${version}`;
|
|
||||||
let newOverrideUrl = override?.url;
|
|
||||||
if (newOverrideUrl && newOverrideUrl.includes("v0") && version !== 0) {
|
|
||||||
newOverrideUrl = newOverrideUrl.replace("v0", versionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullUrl = newOverrideUrl
|
|
||||||
? `https://${newOverrideUrl}/${path.replace(/^\//, "")}`
|
|
||||||
: version === 0
|
|
||||||
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
|
|
||||||
: `${base}/v${version}/${path.replace(/^\//, "")}`;
|
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
|
||||||
Vary: "Accept, Origin",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!options.bypassAuth) {
|
|
||||||
headers.Authorization = `Bearer ${override?.token ?? authToken}`;
|
|
||||||
headers["Access-Control-Allow-Headers"] = "Authorization";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentType !== "none") {
|
|
||||||
headers["Content-Type"] = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (import.meta.client && typeof window !== "undefined") {
|
|
||||||
headers.Origin = window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = (typeof retry === "boolean" ? (retry ? 3 : 1) : retry) + 1;
|
|
||||||
let lastError: Error | null = null;
|
|
||||||
|
|
||||||
while (attempts < maxAttempts) {
|
|
||||||
try {
|
|
||||||
const response = await $fetch<T>(fullUrl, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error as Error;
|
|
||||||
attempts++;
|
|
||||||
|
|
||||||
if (error instanceof FetchError) {
|
|
||||||
const statusCode = error.response?.status;
|
|
||||||
const statusText = error.response?.statusText || "Unknown error";
|
|
||||||
|
|
||||||
let v1Error: V1ErrorInfo | undefined;
|
|
||||||
if (error.data?.error && error.data?.description) {
|
|
||||||
v1Error = {
|
|
||||||
context: errorContext,
|
|
||||||
...error.data,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const errorMessages: { [key: number]: string } = {
|
|
||||||
400: "Bad Request",
|
|
||||||
401: "Unauthorized",
|
|
||||||
403: "Forbidden",
|
|
||||||
404: "Not Found",
|
|
||||||
405: "Method Not Allowed",
|
|
||||||
408: "Request Timeout",
|
|
||||||
429: "Too Many Requests",
|
|
||||||
500: "Internal Server Error",
|
|
||||||
502: "Bad Gateway",
|
|
||||||
503: "Service Unavailable",
|
|
||||||
504: "Gateway Timeout",
|
|
||||||
};
|
|
||||||
|
|
||||||
const message =
|
|
||||||
statusCode && statusCode in errorMessages
|
|
||||||
? errorMessages[statusCode]
|
|
||||||
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
|
||||||
|
|
||||||
const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true;
|
|
||||||
|
|
||||||
if (!isRetryable || attempts >= maxAttempts) {
|
|
||||||
console.error("Fetch error:", error);
|
|
||||||
|
|
||||||
const fetchError = new ModrinthServersFetchError(
|
|
||||||
`[Modrinth Servers] ${message}`,
|
|
||||||
statusCode,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
|
|
||||||
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("Unexpected fetch error:", error);
|
|
||||||
const fetchError = new ModrinthServersFetchError(
|
|
||||||
"[Modrinth Servers] An unexpected error occurred during the fetch operation.",
|
|
||||||
undefined,
|
|
||||||
error as Error,
|
|
||||||
);
|
|
||||||
throw new ModrinthServerError(
|
|
||||||
"Unexpected error during fetch operation",
|
|
||||||
undefined,
|
|
||||||
fetchError,
|
|
||||||
module,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("All retry attempts failed:", lastError);
|
|
||||||
if (lastError instanceof FetchError) {
|
|
||||||
const statusCode = lastError.response?.status;
|
|
||||||
const pyroError = new ModrinthServersFetchError(
|
|
||||||
"Maximum retry attempts reached",
|
|
||||||
statusCode,
|
|
||||||
lastError,
|
|
||||||
);
|
|
||||||
throw new ModrinthServerError("Maximum retry attempts reached", statusCode, pyroError, module);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchError = new ModrinthServersFetchError(
|
|
||||||
"Maximum retry attempts reached",
|
|
||||||
undefined,
|
|
||||||
lastError || undefined,
|
|
||||||
);
|
|
||||||
throw new ModrinthServerError("Maximum retry attempts reached", undefined, fetchError, module);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { formatBytes } from "@modrinth/utils";
|
import { formatBytes } from "~/plugins/shorthands.js";
|
||||||
|
|
||||||
export const fileIsValid = (file, validationOptions) => {
|
export const fileIsValid = (file, validationOptions) => {
|
||||||
const { maxSize, alertOnInvalid } = validationOptions;
|
const { maxSize, alertOnInvalid } = validationOptions;
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
"message": "Subscribe to updates about Modrinth"
|
"message": "Subscribe to updates about Modrinth"
|
||||||
},
|
},
|
||||||
"auth.welcome.description": {
|
"auth.welcome.description": {
|
||||||
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with awazing mods."
|
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with amazing mods."
|
||||||
},
|
},
|
||||||
"auth.welcome.label.tos": {
|
"auth.welcome.label.tos": {
|
||||||
"message": "By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
"message": "By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
||||||
@@ -350,14 +350,11 @@
|
|||||||
"layout.banner.add-email.button": {
|
"layout.banner.add-email.button": {
|
||||||
"message": "Visit account settings"
|
"message": "Visit account settings"
|
||||||
},
|
},
|
||||||
"layout.banner.add-email.description": {
|
|
||||||
"message": "For security reasons, Modrinth needs you to register an email address to your account."
|
|
||||||
},
|
|
||||||
"layout.banner.build-fail.description": {
|
"layout.banner.build-fail.description": {
|
||||||
"message": "This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}"
|
"message": "This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}"
|
||||||
},
|
},
|
||||||
"layout.banner.build-fail.title": {
|
"layout.banner.build-fail.title": {
|
||||||
"message": "Error generating state from API when building."
|
"message": "Error generating state from API when building"
|
||||||
},
|
},
|
||||||
"layout.banner.staging.description": {
|
"layout.banner.staging.description": {
|
||||||
"message": "The staging environment is completely separate from the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance."
|
"message": "The staging environment is completely separate from the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance."
|
||||||
@@ -368,12 +365,12 @@
|
|||||||
"layout.banner.subscription-payment-failed.button": {
|
"layout.banner.subscription-payment-failed.button": {
|
||||||
"message": "Update billing info"
|
"message": "Update billing info"
|
||||||
},
|
},
|
||||||
|
"layout.banner.subscription-payment-failed.title": {
|
||||||
|
"message": "Billing action required"
|
||||||
|
},
|
||||||
"layout.banner.subscription-payment-failed.description": {
|
"layout.banner.subscription-payment-failed.description": {
|
||||||
"message": "One or more subscriptions failed to renew. Please update your payment method to prevent losing access!"
|
"message": "One or more subscriptions failed to renew. Please update your payment method to prevent losing access!"
|
||||||
},
|
},
|
||||||
"layout.banner.subscription-payment-failed.title": {
|
|
||||||
"message": "Billing action required."
|
|
||||||
},
|
|
||||||
"layout.banner.verify-email.action": {
|
"layout.banner.verify-email.action": {
|
||||||
"message": "Re-send verification email"
|
"message": "Re-send verification email"
|
||||||
},
|
},
|
||||||
@@ -1050,23 +1047,32 @@
|
|||||||
"message": "No notices"
|
"message": "No notices"
|
||||||
},
|
},
|
||||||
"servers.plan.large.description": {
|
"servers.plan.large.description": {
|
||||||
"message": "Ideal for 15–25 players, modpacks, or heavy modding."
|
"message": "Ideal for larger communities, modpacks, and heavy modding."
|
||||||
},
|
},
|
||||||
"servers.plan.large.name": {
|
"servers.plan.large.name": {
|
||||||
"message": "Large"
|
"message": "Large"
|
||||||
},
|
},
|
||||||
|
"servers.plan.large.symbol": {
|
||||||
|
"message": "L"
|
||||||
|
},
|
||||||
"servers.plan.medium.description": {
|
"servers.plan.medium.description": {
|
||||||
"message": "Great for 6–15 players and multiple mods."
|
"message": "Great for modded multiplayer and small communities."
|
||||||
},
|
},
|
||||||
"servers.plan.medium.name": {
|
"servers.plan.medium.name": {
|
||||||
"message": "Medium"
|
"message": "Medium"
|
||||||
},
|
},
|
||||||
|
"servers.plan.medium.symbol": {
|
||||||
|
"message": "M"
|
||||||
|
},
|
||||||
"servers.plan.small.description": {
|
"servers.plan.small.description": {
|
||||||
"message": "Perfect for 1–5 friends with a few light mods."
|
"message": "Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding."
|
||||||
},
|
},
|
||||||
"servers.plan.small.name": {
|
"servers.plan.small.name": {
|
||||||
"message": "Small"
|
"message": "Small"
|
||||||
},
|
},
|
||||||
|
"servers.plan.small.symbol": {
|
||||||
|
"message": "S"
|
||||||
|
},
|
||||||
"settings.billing.modal.cancel.action": {
|
"settings.billing.modal.cancel.action": {
|
||||||
"message": "Cancel subscription"
|
"message": "Cancel subscription"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<h1 class="wrap-as-needed">
|
<h1 class="wrap-as-needed">
|
||||||
{{ project.title }}
|
{{ project.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<ProjectStatusBadge :status="project.status" />
|
<Badge :type="project.status" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2>Project settings</h2>
|
<h2>Project settings</h2>
|
||||||
@@ -452,16 +452,6 @@
|
|||||||
{{ formatCategory(currentPlatform) }}.
|
{{ formatCategory(currentPlatform) }}.
|
||||||
</p>
|
</p>
|
||||||
</AutomaticAccordion>
|
</AutomaticAccordion>
|
||||||
<ServersPromo
|
|
||||||
v-if="flags.showProjectPageDownloadModalServersPromo"
|
|
||||||
:link="`/servers#plan`"
|
|
||||||
@close="
|
|
||||||
() => {
|
|
||||||
flags.showProjectPageDownloadModalServersPromo = false;
|
|
||||||
saveFeatureFlags();
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
@@ -505,64 +495,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
|
||||||
v-if="canCreateServerFrom && flags.showProjectPageQuickServerButton"
|
|
||||||
theme="dismissable-prompt"
|
|
||||||
:triggers="[]"
|
|
||||||
:shown="flags.showProjectPageCreateServersTooltip"
|
|
||||||
:auto-hide="false"
|
|
||||||
placement="bottom-start"
|
|
||||||
>
|
|
||||||
<ButtonStyled size="large" circular>
|
|
||||||
<nuxt-link
|
|
||||||
v-tooltip="'Create a server'"
|
|
||||||
:to="`/servers?project=${project.id}#plan`"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
flags.showProjectPageCreateServersTooltip = false;
|
|
||||||
saveFeatureFlags();
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ServerPlusIcon aria-hidden="true" />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<template #popper>
|
|
||||||
<div class="experimental-styles-within flex max-w-60 flex-col gap-1">
|
|
||||||
<div class="flex items-center justify-between gap-4">
|
|
||||||
<h3 class="m-0 flex items-center gap-2 text-base font-bold text-contrast">
|
|
||||||
Create a server
|
|
||||||
<TagItem
|
|
||||||
:style="{
|
|
||||||
'--_color': 'var(--color-brand)',
|
|
||||||
'--_bg-color': 'var(--color-brand-highlight)',
|
|
||||||
}"
|
|
||||||
>New</TagItem
|
|
||||||
>
|
|
||||||
</h3>
|
|
||||||
<ButtonStyled size="small" circular>
|
|
||||||
<button
|
|
||||||
v-tooltip="`Don't show again`"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
flags.showProjectPageCreateServersTooltip = false;
|
|
||||||
saveFeatureFlags();
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<XIcon aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
|
||||||
Modrinth Servers is the easiest way to play with your friends without hassle!
|
|
||||||
</p>
|
|
||||||
<p class="m-0 text-wrap text-sm font-bold text-primary">
|
|
||||||
Starting at $5<span class="text-xs"> / month</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Tooltip>
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
size="large"
|
size="large"
|
||||||
@@ -918,14 +850,12 @@ import {
|
|||||||
ReportIcon,
|
ReportIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
ServerPlusIcon,
|
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
TagsIcon,
|
TagsIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
VersionIcon,
|
VersionIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
ModrinthIcon,
|
ModrinthIcon,
|
||||||
XIcon,
|
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -940,27 +870,17 @@ import {
|
|||||||
ProjectSidebarCreators,
|
ProjectSidebarCreators,
|
||||||
ProjectSidebarDetails,
|
ProjectSidebarDetails,
|
||||||
ProjectSidebarLinks,
|
ProjectSidebarLinks,
|
||||||
ProjectStatusBadge,
|
|
||||||
ScrollablePanel,
|
ScrollablePanel,
|
||||||
TagItem,
|
|
||||||
ServersPromo,
|
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||||
import {
|
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
|
||||||
formatCategory,
|
|
||||||
formatProjectType,
|
|
||||||
isRejected,
|
|
||||||
isStaff,
|
|
||||||
isUnderReview,
|
|
||||||
renderString,
|
|
||||||
} from "@modrinth/utils";
|
|
||||||
import { navigateTo } from "#app";
|
import { navigateTo } from "#app";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Tooltip } from "floating-vue";
|
|
||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import Accordion from "~/components/ui/Accordion.vue";
|
||||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||||
|
import Badge from "~/components/ui/Badge.vue";
|
||||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||||
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
||||||
@@ -971,7 +891,6 @@ import NavTabs from "~/components/ui/NavTabs.vue";
|
|||||||
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
|
import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
|
||||||
import { userCollectProject } from "~/composables/user.js";
|
import { userCollectProject } from "~/composables/user.js";
|
||||||
import { reportProject } from "~/utils/report-helpers.ts";
|
import { reportProject } from "~/utils/report-helpers.ts";
|
||||||
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
|
|
||||||
|
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
@@ -1367,7 +1286,7 @@ featuredVersions.value.sort((a, b) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const projectTypeDisplay = computed(() =>
|
const projectTypeDisplay = computed(() =>
|
||||||
formatProjectType(
|
data.$formatProjectType(
|
||||||
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
|
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1385,10 +1304,6 @@ const description = computed(
|
|||||||
} by ${members.value.find((x) => x.is_owner)?.user?.username || "a Creator"} on Modrinth`,
|
} by ${members.value.find((x) => x.is_owner)?.user?.username || "a Creator"} on Modrinth`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const canCreateServerFrom = computed(() => {
|
|
||||||
return project.value.project_type === "modpack" && project.value.server_side !== "unsupported";
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!route.name.startsWith("type-id-settings")) {
|
if (!route.name.startsWith("type-id-settings")) {
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: () => title.value,
|
title: () => title.value,
|
||||||
@@ -1757,33 +1672,4 @@ const navLinks = computed(() => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.servers-popup {
|
|
||||||
box-shadow:
|
|
||||||
0 0 12px 1px rgba(0, 175, 92, 0.6),
|
|
||||||
var(--shadow-floating);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
border-bottom: 6px solid var(--color-button-bg);
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
top: -7px;
|
|
||||||
left: 17px;
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 5px solid transparent;
|
|
||||||
border-right: 5px solid transparent;
|
|
||||||
border-bottom: 5px solid var(--color-raised-bg);
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
left: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<span class="label__title">Client-side</span>
|
<span class="label__title">Client-side</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Select based on if the
|
Select based on if the
|
||||||
{{ formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||||
client side. Just because a mod works in Singleplayer doesn't mean it has actual
|
client side. Just because a mod works in Singleplayer doesn't mean it has actual
|
||||||
client-side functionality.
|
client-side functionality.
|
||||||
</span>
|
</span>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<span class="label__title">Server-side</span>
|
<span class="label__title">Server-side</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Select based on if the
|
Select based on if the
|
||||||
{{ formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||||
<strong>logical</strong> server. Remember that Singleplayer contains an integrated
|
<strong>logical</strong> server. Remember that Singleplayer contains an integrated
|
||||||
server.
|
server.
|
||||||
</span>
|
</span>
|
||||||
@@ -239,10 +239,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { formatProjectStatus, formatProjectType } from "@modrinth/utils";
|
import { formatProjectStatus } from "@modrinth/utils";
|
||||||
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
||||||
import { Multiselect } from "vue-multiselect";
|
import { Multiselect } from "vue-multiselect";
|
||||||
import { ConfirmModal, Avatar } from "@modrinth/ui";
|
import { ConfirmModal } from "@modrinth/ui";
|
||||||
|
import Avatar from "~/components/ui/Avatar.vue";
|
||||||
import FileInput from "~/components/ui/FileInput.vue";
|
import FileInput from "~/components/ui/FileInput.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
Loaded 100 of 444 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user