Compare commits
47 Commits
fetch/incl
...
alex/auth-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f07bc86711 | ||
|
|
23f0c1dbf7 | ||
|
|
8abbc021ea | ||
|
|
ed8ff79809 | ||
|
|
f210de563d | ||
|
|
e410a07cac | ||
|
|
9f93cd8705 | ||
|
|
dd391be095 | ||
|
|
f84f8c1c2b | ||
|
|
301967d204 | ||
|
|
c9b98a6154 | ||
|
|
ab8e474339 | ||
|
|
8a26011e76 | ||
|
|
d4de1dc9a1 | ||
|
|
4e3bd4e282 | ||
|
|
d24528f6a6 | ||
|
|
1b1d41605b | ||
|
|
6955731def | ||
|
|
4386891716 | ||
|
|
6741aba880 | ||
|
|
ee8ee7af82 | ||
|
|
a2e323c9ee | ||
|
|
f8fb23e05f | ||
|
|
a3839461cf | ||
|
|
858c7e393f | ||
|
|
0278241006 | ||
|
|
3afb682fc6 | ||
|
|
06f1df1995 | ||
|
|
3489771d2e | ||
|
|
448ae5a2b7 | ||
|
|
72340790e5 | ||
|
|
c9423fe478 | ||
|
|
02a850ae63 | ||
|
|
ede6d0c3cc | ||
|
|
7685989a8c | ||
|
|
4e8ebb5e5c | ||
|
|
3f77ab19ed | ||
|
|
d3d0c8c523 | ||
|
|
4e093131f3 | ||
|
|
6ca8a4e5fd | ||
|
|
63b15ded60 | ||
|
|
85e65aeffe | ||
|
|
ad44398492 | ||
|
|
a4ba41bf15 | ||
|
|
4441be5380 | ||
|
|
c0accb42fa | ||
|
|
7223c2b197 |
@@ -1,6 +1,6 @@
|
|||||||
# Windows has stack overflows when calling from Tauri, so we increase compiler size
|
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
|
|||||||
14
.github/workflows/theseus-release.yml
vendored
14
.github/workflows/theseus-release.yml
vendored
@@ -32,16 +32,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Rust setup (mac)
|
- name: Rust setup (mac)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
|
|
||||||
- name: Rust setup
|
- name: Rust setup
|
||||||
if: "!startsWith(matrix.platform, 'macos')"
|
if: "!startsWith(matrix.platform, 'macos')"
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
|
|
||||||
- name: Setup rust cache
|
- name: Setup rust cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -72,10 +72,10 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-rust-target-
|
${{ runner.os }}-rust-target-
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: .nvmrc
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: Install pnpm via corepack
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
98
.github/workflows/turbo-ci.yml
vendored
98
.github/workflows/turbo-ci.yml
vendored
@@ -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,71 +10,69 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build, Test, and Lint
|
name: Lint and Test
|
||||||
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: Cache turbo build setup
|
- name: 🧰 Install build dependencies
|
||||||
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 -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||||
|
|
||||||
- name: Setup Node.JS environment
|
- name: 🧰 Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: 🧰 Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: .nvmrc
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: 🧰 Setup Rust toolchain
|
||||||
shell: bash
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
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:
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
rustflags: ''
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
components: clippy, rustfmt
|
||||||
restore-keys: |
|
cache: false
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: 🧰 Setup nextest
|
||||||
|
uses: taiki-e/install-action@nextest
|
||||||
|
|
||||||
|
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||||
|
# back to a cached cargo install
|
||||||
|
- name: 🧰 Setup cargo-sqlx
|
||||||
|
uses: 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: Build
|
- name: ⚙️ Start services
|
||||||
run: pnpm build
|
run: docker compose up --wait
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
|
|
||||||
- name: Lint
|
- name: ⚙️ Setup Labrinth environment and database
|
||||||
run: pnpm lint
|
working-directory: apps/labrinth
|
||||||
env:
|
run: |
|
||||||
SQLX_OFFLINE: true
|
cp .env.local .env
|
||||||
|
sqlx database setup
|
||||||
|
|
||||||
- name: Start docker compose
|
- name: 🔍 Lint and test
|
||||||
run: docker compose up -d
|
run: pnpm run ci
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: pnpm test
|
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
|
||||||
|
|||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
|||||||
<?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>
|
||||||
|
|||||||
420
Cargo.lock
generated
420
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
94
Cargo.toml
94
Cargo.toml
@@ -10,6 +10,9 @@ 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"
|
||||||
@@ -21,7 +24,8 @@ 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-compression = { version = "0.4.23", default-features = false }
|
async_zip = "0.0.17"
|
||||||
|
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",
|
||||||
@@ -31,14 +35,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.0"
|
bitflags = "2.9.1"
|
||||||
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.38"
|
clap = "4.5.40"
|
||||||
clickhouse = "0.13.2"
|
clickhouse = "0.13.3"
|
||||||
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" }
|
||||||
@@ -50,16 +54,18 @@ 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.1"
|
flate2 = "1.1.2"
|
||||||
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"
|
||||||
|
heck = "0.5.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.11"
|
hyper-util = "0.1.14"
|
||||||
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"
|
||||||
@@ -67,7 +73,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.16", default-features = false, features = [
|
lettre = { version = "0.11.17", default-features = false, features = [
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
@@ -89,19 +95,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"
|
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.15", default-features = false }
|
reqwest = { version = "0.12.19", default-features = false }
|
||||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
|
||||||
"fail-on-err",
|
|
||||||
"tags",
|
|
||||||
"tokio-rustls-tls",
|
|
||||||
] }
|
|
||||||
rust_decimal = { version = "1.37.1", features = [
|
rust_decimal = { version = "1.37.1", features = [
|
||||||
"serde-with-float",
|
"serde-with-float",
|
||||||
"serde-with-str",
|
"serde-with-str",
|
||||||
] }
|
] }
|
||||||
rust_iso3166 = "0.1.14"
|
rust_iso3166 = "0.1.14"
|
||||||
|
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||||
|
"fail-on-err",
|
||||||
|
"tags",
|
||||||
|
"tokio-rustls-tls",
|
||||||
|
] }
|
||||||
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",
|
||||||
@@ -113,26 +119,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.5", default-features = false }
|
sqlx = { version = "0.8.6", default-features = false }
|
||||||
sysinfo = { version = "0.35.1", default-features = false }
|
sysinfo = { version = "0.35.2", 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.2.1"
|
tauri-plugin-deep-link = "2.3.0"
|
||||||
tauri-plugin-dialog = "2.2.1"
|
tauri-plugin-dialog = "2.2.2"
|
||||||
tauri-plugin-opener = "2.2.6"
|
tauri-plugin-opener = "2.2.7"
|
||||||
tauri-plugin-os = "2.2.1"
|
tauri-plugin-os = "2.2.1"
|
||||||
tauri-plugin-single-instance = "2.2.3"
|
tauri-plugin-single-instance = "2.2.4"
|
||||||
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",
|
||||||
@@ -143,7 +149,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.0"
|
tokio = "1.45.1"
|
||||||
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"
|
||||||
@@ -153,14 +159,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.16.0"
|
uuid = "1.17.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 = "3.0.0", default-features = false, features = [
|
zip = { version = "4.0.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -168,6 +174,44 @@ zip = { version = "3.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,18 +9,19 @@
|
|||||||
"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-os": "^2.2.1",
|
|
||||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||||
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@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",
|
||||||
@@ -39,11 +40,12 @@
|
|||||||
"@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.1.1",
|
"eslint-plugin-turbo": "^2.5.4",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.74.1",
|
"sass": "^1.74.1",
|
||||||
@@ -51,8 +53,7 @@
|
|||||||
"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"
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
size="36px"
|
size="36px"
|
||||||
:src="
|
:src="
|
||||||
selectedAccount
|
selectedAccount
|
||||||
? `https://mc-heads.net/avatar/${selectedAccount.id}/128`
|
? `https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`
|
||||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<span>{{ selectedAccount ? selectedAccount.username : 'Select account' }}</span>
|
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
||||||
<span class="text-secondary text-xs">Minecraft account</span>
|
<span class="text-secondary text-xs">Minecraft account</span>
|
||||||
</div>
|
</div>
|
||||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||||
@@ -28,12 +28,17 @@
|
|||||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||||
>
|
>
|
||||||
<div v-if="selectedAccount" class="selected account">
|
<div v-if="selectedAccount" class="selected account">
|
||||||
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.id}/128`" />
|
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`" />
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ selectedAccount.username }}</h4>
|
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||||
<p>Selected</p>
|
<p>Selected</p>
|
||||||
</div>
|
</div>
|
||||||
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
|
<Button
|
||||||
|
v-tooltip="'Log out'"
|
||||||
|
icon-only
|
||||||
|
color="raised"
|
||||||
|
@click="logout(selectedAccount.profile.id)"
|
||||||
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,12 +49,12 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||||
<div v-for="account in displayAccounts" :key="account.id" class="account-row">
|
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||||
<Button class="option account" @click="setAccount(account)">
|
<Button class="option account" @click="setAccount(account)">
|
||||||
<Avatar :src="`https://mc-heads.net/avatar/${account.id}/128`" class="icon" />
|
<Avatar :src="`https://mc-heads.net/avatar/${account.profile.id}/128`" class="icon" />
|
||||||
<p>{{ account.username }}</p>
|
<p>{{ account.profile.name }}</p>
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
|
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,16 +106,16 @@ defineExpose({
|
|||||||
await refreshValues()
|
await refreshValues()
|
||||||
|
|
||||||
const displayAccounts = computed(() =>
|
const displayAccounts = computed(() =>
|
||||||
accounts.value.filter((account) => defaultUser.value !== account.id),
|
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedAccount = computed(() =>
|
const selectedAccount = computed(() =>
|
||||||
accounts.value.find((account) => account.id === defaultUser.value),
|
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function setAccount(account) {
|
async function setAccount(account) {
|
||||||
defaultUser.value = account.id
|
defaultUser.value = account.profile.id
|
||||||
await set_default_user(account.id).catch(handleError)
|
await set_default_user(account.profile.id).catch(handleError)
|
||||||
emit('change')
|
emit('change')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ async function loginMinecraft() {
|
|||||||
const loggedIn = await login_flow()
|
const loggedIn = await login_flow()
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await set_default_user(loggedIn.id).catch(handleError)
|
await set_default_user(loggedIn.profile.id).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ const editProfileObject = computed(() => {
|
|||||||
hooks?: Hooks
|
hooks?: Hooks
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (overrideHooks.value) {
|
// When hooks are not overridden per-instance, we want to clear them
|
||||||
editProfile.hooks = hooks.value
|
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
||||||
import {
|
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||||
set_world_display_status,
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
getWorldIdentifier,
|
|
||||||
showWorldInFolder,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { formatNumber } from '@modrinth/utils'
|
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -49,6 +45,7 @@ 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(
|
||||||
@@ -108,20 +105,6 @@ 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({
|
||||||
@@ -394,8 +377,7 @@ const messages = defineMessages({
|
|||||||
{
|
{
|
||||||
id: 'open-folder',
|
id: 'open-folder',
|
||||||
shown: world.type === 'singleplayer',
|
shown: world.type === 'singleplayer',
|
||||||
action: () =>
|
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
divider: true,
|
divider: true,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ 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,7 +32,11 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-if="instance.install_stage.includes('installing')"
|
v-if="
|
||||||
|
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
||||||
|
instance.install_stage,
|
||||||
|
)
|
||||||
|
"
|
||||||
color="brand"
|
color="brand"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
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>
|
||||||
@@ -151,6 +152,7 @@ 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 = "2024"
|
edition.workspace = true
|
||||||
|
|
||||||
# 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,3 +9,6 @@ edition = "2024"
|
|||||||
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 --all-features -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix",
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo test"
|
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
|||||||
|
|
||||||
let credentials = minecraft_auth::finish_login(&input, login).await?;
|
let credentials = minecraft_auth::finish_login(&input, login).await?;
|
||||||
|
|
||||||
println!("Logged in user {}.", credentials.username);
|
println!(
|
||||||
|
"Logged in user {}.",
|
||||||
|
credentials.maybe_online_profile().await.name
|
||||||
|
);
|
||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ 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 = "2024"
|
edition.workspace = true
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { workspace = true, features = ["codegen"] }
|
tauri-build = { workspace = true, features = ["codegen"] }
|
||||||
@@ -56,3 +55,6 @@ 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
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ fn main() {
|
|||||||
"profile_update_managed_modrinth_version",
|
"profile_update_managed_modrinth_version",
|
||||||
"profile_repair_managed_modrinth",
|
"profile_repair_managed_modrinth",
|
||||||
"profile_run",
|
"profile_run",
|
||||||
"profile_run_credentials",
|
|
||||||
"profile_kill",
|
"profile_kill",
|
||||||
"profile_edit",
|
"profile_edit",
|
||||||
"profile_edit_icon",
|
"profile_edit_icon",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app",
|
"name": "@modrinth/app",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tauri build",
|
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
|
"build": "tauri build",
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"test": "cargo test",
|
"test": "cargo nextest run --all-targets --no-fail-fast",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix"
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "2.5.0"
|
"@tauri-apps/cli": "2.5.0"
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
|||||||
profile_update_managed_modrinth_version,
|
profile_update_managed_modrinth_version,
|
||||||
profile_repair_managed_modrinth,
|
profile_repair_managed_modrinth,
|
||||||
profile_run,
|
profile_run,
|
||||||
profile_run_credentials,
|
|
||||||
profile_kill,
|
profile_kill,
|
||||||
profile_edit,
|
profile_edit,
|
||||||
profile_edit_icon,
|
profile_edit_icon,
|
||||||
@@ -256,22 +255,6 @@ pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
|||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run Minecraft using a profile using chosen credentials
|
|
||||||
// Returns the UUID, which can be used to poll
|
|
||||||
// for the actual Child in the state.
|
|
||||||
// invoke('plugin:profile|profile_run_credentials', {path, credentials})')
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn profile_run_credentials(
|
|
||||||
path: &str,
|
|
||||||
credentials: Credentials,
|
|
||||||
) -> Result<ProcessMetadata> {
|
|
||||||
let process =
|
|
||||||
profile::run_credentials(path, &credentials, &QuickPlayType::None)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(process)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_kill(path: &str) -> Result<()> {
|
pub async fn profile_kill(path: &str) -> Result<()> {
|
||||||
profile::kill(path).await?;
|
profile::kill(path).await?;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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 result.iter_mut() {
|
for world in &mut result {
|
||||||
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 result.iter_mut() {
|
for world in &mut result {
|
||||||
adapt_world_icon(&app_handle, world);
|
adapt_world_icon(&app_handle, world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ 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");
|
||||||
@@ -22,7 +23,5 @@ 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.clone();
|
let mtx_copy = payload.payload;
|
||||||
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,7 +229,6 @@ 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"))]
|
||||||
@@ -273,22 +272,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()
|
||||||
.filter_map(|url| url.to_file_path().ok())
|
.find_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.clone();
|
let mtx_copy = payload.payload;
|
||||||
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;
|
||||||
|
|||||||
14
apps/app/turbo.jsonc
Normal file
14
apps/app/turbo.jsonc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/turbo/schema.json",
|
||||||
|
"extends": ["//"],
|
||||||
|
"tasks": {
|
||||||
|
// Running Clippy and tests on a Tauri application requires
|
||||||
|
// the frontend to be built at least once first
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "daedalus_client"
|
name = "daedalus_client"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
authors = ["Jai A <jai@modrinth.com>"]
|
authors = ["Jai A <jai@modrinth.com>"]
|
||||||
edition = "2024"
|
edition.workspace = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -28,3 +28,6 @@ tracing-error.workspace = true
|
|||||||
|
|
||||||
tracing.workspace = true
|
tracing.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.86.0 AS build
|
FROM rust:1.87.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 --all-features -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix",
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo test"
|
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/daedalus": "workspace:*"
|
"@modrinth/daedalus": "workspace:*"
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ pub async fn fetch(
|
|||||||
if modrinth_version
|
if modrinth_version
|
||||||
.original_sha1
|
.original_sha1
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|x| x == &version.sha1)
|
.is_some_and(|x| x == &version.sha1)
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
{
|
||||||
existing_versions.push(modrinth_version);
|
existing_versions.push(modrinth_version);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro check && astro build",
|
"lint": "astro check",
|
||||||
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
@@ -18,4 +19,4 @@
|
|||||||
"starlight-openapi": "^0.14.0",
|
"starlight-openapi": "^0.14.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"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",
|
||||||
|
|||||||
128
apps/frontend/src/components/ui/OptionGroup.vue
Normal file
128
apps/frontend/src/components/ui/OptionGroup.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<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>
|
||||||
@@ -133,7 +133,7 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
|||||||
props,
|
props,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const returnTopN = 5;
|
const returnTopN = 15;
|
||||||
|
|
||||||
const listEntries = series
|
const listEntries = series
|
||||||
.map((value, index) => [
|
.map((value, index) => [
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<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: {
|
||||||
@@ -53,13 +54,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)}`, () =>
|
||||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
fetchSegmented(userIds, (ids) => `users?ids=${asEncodedJsonArray(ids)}`),
|
||||||
),
|
),
|
||||||
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
||||||
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`),
|
fetchSegmented(versionIds, (ids) => `versions?ids=${asEncodedJsonArray(ids)}`),
|
||||||
),
|
),
|
||||||
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||||
useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`),
|
fetchSegmented(threadIds, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -70,7 +71,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)}`, () =>
|
||||||
useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`),
|
fetchSegmented(projectIds, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
reports.value = rawReports.map((report) => {
|
reports.value = rawReports.map((report) => {
|
||||||
|
|||||||
@@ -45,9 +45,11 @@
|
|||||||
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: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
@@ -64,7 +66,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) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ const createBackup = async () => {
|
|||||||
hideModal();
|
hideModal();
|
||||||
await props.server.refresh();
|
await props.server.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||||
isRateLimited.value = true;
|
isRateLimited.value = true;
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -20,13 +20,9 @@
|
|||||||
<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 { Server } from "~/composables/pyroServers";
|
import type { Backup } from "@modrinth/utils";
|
||||||
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;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
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 } from "vue";
|
import { ref } from "vue";
|
||||||
import type { Backup } from "~/composables/pyroServers.ts";
|
import type { Backup } from "@modrinth/utils";
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags();
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|||||||
@@ -48,10 +48,11 @@
|
|||||||
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 { Server } from "~/composables/pyroServers";
|
import type { Backup } from "@modrinth/utils";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
@@ -70,7 +71,7 @@ const nameExists = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return props.server.backups.data.some(
|
return props.server.backups.data.some(
|
||||||
(backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
<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 { Server } from "~/composables/pyroServers";
|
import type { Backup } from "@modrinth/utils";
|
||||||
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: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
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 type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["backups"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
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 Version } from "@modrinth/utils";
|
import { formatCategory, formatVersionsForDisplay, type Mod, 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, {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
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 type { FSModule } from "~/composables/pyroServers.ts";
|
import { FSModule } from "~/composables/servers/modules/fs.ts";
|
||||||
|
|
||||||
interface UploadItem {
|
interface UploadItem {
|
||||||
file: File;
|
file: File;
|
||||||
|
|||||||
@@ -75,13 +75,14 @@
|
|||||||
<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, type Server } from "~/composables/pyroServers.ts";
|
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const cf = ref(false);
|
const cf = ref(false);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>();
|
||||||
@@ -110,24 +111,18 @@ 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 ServersError(
|
new ModrinthServersFetchError(
|
||||||
"Could not find CurseForge modpack at that URL.",
|
"Could not find CurseForge modpack at that URL.",
|
||||||
undefined,
|
404,
|
||||||
undefined,
|
new Error(`No modpack found at ${url.value}`),
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
context: "Error installing modpack",
|
|
||||||
error: `url: ${url.value}`,
|
|
||||||
description: "Could not find CurseForge modpack at that URL.",
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const props = defineProps<{
|
|||||||
loader: string | null;
|
loader: string | null;
|
||||||
loader_version: string | null;
|
loader_version: string | null;
|
||||||
};
|
};
|
||||||
|
ignoreCurrentInstallation?: boolean;
|
||||||
isInstalling?: boolean;
|
isInstalling?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
<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,14 +120,12 @@ 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: ServerAction;
|
action: ServerPowerAction;
|
||||||
nextState: ServerState;
|
nextState: ServerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +140,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "action", action: ServerAction): void;
|
(e: "action", action: ServerPowerAction): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -170,7 +168,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: Record<ServerState, string> = {
|
const states: Partial<Record<ServerState, string>> = {
|
||||||
starting: "Starting...",
|
starting: "Starting...",
|
||||||
restarting: "Restarting...",
|
restarting: "Restarting...",
|
||||||
running: "Restart",
|
running: "Restart",
|
||||||
@@ -193,7 +191,7 @@ const menuOptions = computed(() => [
|
|||||||
id: "kill",
|
id: "kill",
|
||||||
label: "Kill server",
|
label: "Kill server",
|
||||||
icon: SlashIcon,
|
icon: SlashIcon,
|
||||||
action: () => initiateAction("kill"),
|
action: () => initiateAction("Kill"),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
@@ -221,17 +219,17 @@ async function copyId() {
|
|||||||
await navigator.clipboard.writeText(serverId as string);
|
await navigator.clipboard.writeText(serverId as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initiateAction(action: ServerAction) {
|
function initiateAction(action: ServerPowerAction) {
|
||||||
if (!canTakeAction.value) return;
|
if (!canTakeAction.value) return;
|
||||||
|
|
||||||
const stateMap: Record<ServerAction, ServerState> = {
|
const stateMap: Record<ServerPowerAction, 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;
|
||||||
@@ -249,7 +247,7 @@ function initiateAction(action: ServerAction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePrimaryAction() {
|
function handlePrimaryAction() {
|
||||||
initiateAction(isRunning.value ? "restart" : "start");
|
initiateAction(isRunning.value ? "Restart" : "Start");
|
||||||
}
|
}
|
||||||
|
|
||||||
function executePowerAction() {
|
function executePowerAction() {
|
||||||
@@ -263,7 +261,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 "~/types/servers";
|
import type { ServerState } from "@modrinth/utils";
|
||||||
|
|
||||||
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 = {
|
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
|
||||||
running: "Running",
|
running: "Running",
|
||||||
stopped: "",
|
stopped: "",
|
||||||
crashed: "Crashed",
|
crashed: "Crashed",
|
||||||
@@ -63,7 +63,10 @@ defineProps<{
|
|||||||
const isExpanded = ref(false);
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
function getStatusClass(state: ServerState) {
|
function getStatusClass(state: ServerState) {
|
||||||
return STATUS_CLASSES[state] ?? STATUS_CLASSES.unknown;
|
if (state in STATUS_CLASSES) {
|
||||||
|
return STATUS_CLASSES[state as keyof typeof STATUS_CLASSES];
|
||||||
|
}
|
||||||
|
return STATUS_CLASSES.unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusText(state: ServerState) {
|
function getStatusText(state: ServerState) {
|
||||||
|
|||||||
@@ -7,16 +7,17 @@
|
|||||||
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" @click="clearSearch">
|
<ButtonStyled v-if="searchInput && !loading" @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"
|
v-if="pyroConsole.filteredOutput.value.length && searchInput && !loading"
|
||||||
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 }}
|
||||||
@@ -29,11 +30,13 @@
|
|||||||
: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"
|
v-if="cosmetics.advancedRendering && !loading"
|
||||||
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"
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="!loading"
|
||||||
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
|
||||||
@@ -79,6 +82,7 @@
|
|||||||
</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"
|
||||||
@@ -118,7 +122,12 @@
|
|||||||
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 data-pyro-terminal-virtual-height-watcher :style="{ height: `${totalHeight}px` }">
|
<div v-if="loading" class="h-full w-full" />
|
||||||
|
<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
|
||||||
@@ -205,6 +214,7 @@
|
|||||||
<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"
|
||||||
@@ -217,7 +227,7 @@
|
|||||||
|
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-if="hasSelection || isSingleLineSelected"
|
v-if="(hasSelection || isSingleLineSelected) && !loading"
|
||||||
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 }"
|
||||||
>
|
>
|
||||||
@@ -247,7 +257,7 @@
|
|||||||
|
|
||||||
<Transition name="scroll-to-bottom">
|
<Transition name="scroll-to-bottom">
|
||||||
<button
|
<button
|
||||||
v-if="bottomThreshold > 0 && !isScrolledToBottom"
|
v-if="bottomThreshold > 0 && !isScrolledToBottom && !loading"
|
||||||
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"
|
||||||
@@ -291,13 +301,14 @@ 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 { usePyroConsole } from "~/store/console.ts";
|
import { useModrinthServersConsole } 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;
|
||||||
@@ -307,8 +318,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 = usePyroConsole();
|
const pyroConsole = useModrinthServersConsole();
|
||||||
const consoleOutput = pyroConsole.output;
|
const consoleOutput = computed(() => (props.loading ? [] : pyroConsole.output.value));
|
||||||
|
|
||||||
const scrollContainer = ref<HTMLElement | null>(null);
|
const scrollContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -69,10 +69,11 @@
|
|||||||
<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 type { Server } from "~/composables/pyroServers";
|
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
project: any;
|
project: any;
|
||||||
versions: any[];
|
versions: any[];
|
||||||
currentVersion?: any;
|
currentVersion?: any;
|
||||||
@@ -98,8 +99,7 @@ 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 PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -116,11 +116,12 @@
|
|||||||
<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 type { Server } from "~/composables/pyroServers";
|
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
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: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ const handleReinstall = async () => {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -127,7 +127,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
<div
|
||||||
|
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
|
||||||
@@ -146,7 +149,10 @@
|
|||||||
<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 :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
<BackupWarning
|
||||||
|
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">
|
||||||
@@ -194,9 +200,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 { Server } from "~/composables/pyroServers";
|
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
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();
|
||||||
|
|
||||||
@@ -214,9 +220,10 @@ type VersionMap = Record<string, LoaderVersion[]>;
|
|||||||
type VersionCache = Record<string, any>;
|
type VersionCache = Record<string, any>;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
currentLoader: Loaders | undefined;
|
currentLoader: Loaders | undefined;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
|
initialSetup?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -313,7 +320,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") {
|
||||||
@@ -451,12 +458,11 @@ 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,
|
||||||
hardReset.value,
|
props.initialSetup ? true : hardReset.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit("reinstall", {
|
emit("reinstall", {
|
||||||
@@ -467,7 +473,7 @@ const handleReinstall = async () => {
|
|||||||
|
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && (error as any)?.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 type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
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: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const saveAndRestart = async () => {
|
const saveAndRestart = async () => {
|
||||||
|
|||||||
278
apps/frontend/src/components/ui/servers/ServerInstallation.vue
Normal file
278
apps/frontend/src/components/ui/servers/ServerInstallation.vue
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<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,7 +43,14 @@
|
|||||||
</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"
|
||||||
@@ -73,13 +80,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, HammerIcon, LockIcon } from "@modrinth/assets";
|
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
|
||||||
import type { Project, Server } from "~/types/servers";
|
import type { Project, Server } from "@modrinth/utils";
|
||||||
|
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 usePyroServer(props.server_id, ["general"]);
|
await useModrinthServers(props.server_id, ["general"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showGameLabel = computed(() => !!props.game);
|
const showGameLabel = computed(() => !!props.game);
|
||||||
@@ -102,8 +110,9 @@ 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 usePyroServer(props.server_id!, ["general"]);
|
await useModrinthServers(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: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
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"
|
||||||
@@ -18,7 +20,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"
|
v-if="metric.warning && !loading"
|
||||||
v-tooltip="metric.warning"
|
v-tooltip="metric.warning"
|
||||||
class="size-5"
|
class="size-5"
|
||||||
:style="{ color: 'var(--color-orange)' }"
|
:style="{ color: 'var(--color-orange)' }"
|
||||||
@@ -28,51 +30,76 @@
|
|||||||
<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 :is="metric.icon" class="absolute right-10 top-10 z-10" />
|
<component
|
||||||
<ClientOnly>
|
:is="metric.icon"
|
||||||
<VueApexCharts
|
class="absolute right-10 top-10 z-10 size-8"
|
||||||
v-if="metric.showGraph"
|
style="width: 2rem; height: 2rem"
|
||||||
type="area"
|
/>
|
||||||
height="142"
|
|
||||||
:options="getChartOptions(metric.warning)"
|
<div class="chart-space absolute bottom-0 left-0 right-0">
|
||||||
:series="[{ name: metric.title, data: metric.data }]"
|
<ClientOnly>
|
||||||
class="chart absolute bottom-0 left-0 right-0 w-full opacity-0"
|
<VueApexCharts
|
||||||
/>
|
v-if="metric.showGraph && !loading"
|
||||||
</ClientOnly>
|
type="area"
|
||||||
|
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>
|
||||||
|
|
||||||
<NuxtLink
|
<component
|
||||||
:to="`/servers/manage/${serverId}/files`"
|
:is="loading ? 'div' : 'NuxtLink'"
|
||||||
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"
|
:to="loading ? undefined : `/servers/manage/${serverId}/files`"
|
||||||
|
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">
|
||||||
{{ formatBytes(stats.storage_usage_bytes) }}
|
{{ loading ? "0 B" : 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" />
|
||||||
</NuxtLink>
|
</component>
|
||||||
</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, DBIcon, IssuesIcon } from "@modrinth/assets";
|
import { FolderOpenIcon, CPUIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
import type { Stats } from "~/types/servers";
|
import type { Stats } from "@modrinth/utils";
|
||||||
|
|
||||||
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 = defineProps<{ data: Stats }>();
|
const props = withDefaults(defineProps<{ data?: Stats; loading?: boolean }>(), {
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
const stats = shallowRef(props.data.current);
|
const stats = shallowRef(
|
||||||
|
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"];
|
||||||
@@ -94,6 +121,29 @@ 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,
|
||||||
@@ -119,7 +169,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: DBIcon,
|
icon: DatabaseIcon,
|
||||||
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,
|
||||||
@@ -127,7 +177,7 @@ const metrics = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const getChartOptions = (hasWarning: string | null) => ({
|
const getChartOptions = (hasWarning: string | null, index: number) => ({
|
||||||
chart: {
|
chart: {
|
||||||
type: "area",
|
type: "area",
|
||||||
animations: { enabled: false },
|
animations: { enabled: false },
|
||||||
@@ -139,6 +189,10 @@ const getChartOptions = (hasWarning: string | null) => ({
|
|||||||
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: {
|
||||||
@@ -172,24 +226,26 @@ const getChartOptions = (hasWarning: string | null) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.data.current,
|
() => props.data?.current,
|
||||||
(newStats) => {
|
(newStats) => {
|
||||||
stats.value = newStats;
|
if (newStats) {
|
||||||
|
stats.value = newStats;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart {
|
.chart-space {
|
||||||
animation: fadeIn 0.2s ease-out 0.2s forwards;
|
height: 142px;
|
||||||
|
width: calc(100% + 48px);
|
||||||
margin-left: -24px;
|
margin-left: -24px;
|
||||||
margin-right: -24px;
|
margin-right: -24px;
|
||||||
width: calc(100% + 48px) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.chart {
|
||||||
to {
|
width: 100% !important;
|
||||||
opacity: 1;
|
height: 142px !important;
|
||||||
}
|
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 "~/types/servers";
|
import type { Loaders } from "@modrinth/utils";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
loader: Loaders;
|
loader: Loaders;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled, ServersSpecs } 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 } = useVIntl();
|
const { formatMessage, locale } = 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,15 +30,11 @@ 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:
|
defaultMessage: "Perfect for 1–5 friends with a few light mods.",
|
||||||
"Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding.",
|
|
||||||
}),
|
}),
|
||||||
|
mostPopular: false,
|
||||||
},
|
},
|
||||||
medium: {
|
medium: {
|
||||||
buttonColor: "green",
|
buttonColor: "green",
|
||||||
@@ -48,14 +44,11 @@ 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 modded multiplayer and small communities.",
|
defaultMessage: "Great for 6–15 players and multiple mods.",
|
||||||
}),
|
}),
|
||||||
|
mostPopular: true,
|
||||||
},
|
},
|
||||||
large: {
|
large: {
|
||||||
buttonColor: "purple",
|
buttonColor: "purple",
|
||||||
@@ -65,14 +58,11 @@ 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 larger communities, modpacks, and heavy modding.",
|
defaultMessage: "Ideal for 15–25 players, modpacks, or heavy modding.",
|
||||||
}),
|
}),
|
||||||
|
mostPopular: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,42 +73,30 @@ 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 lowStock = computed(() => {
|
const billingMonths = computed(() => {
|
||||||
return !props.capacity || props.capacity < 8;
|
if (props.interval === "yearly") {
|
||||||
});
|
return 12;
|
||||||
|
} else if (props.interval === "quarterly") {
|
||||||
const formattedRam = computed(() => {
|
return 3;
|
||||||
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 pt-12 lg:w-1/3">
|
<li class="relative flex w-full flex-col justify-between">
|
||||||
<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="
|
||||||
plan === 'medium'
|
plans[plan].mostPopular
|
||||||
? {
|
? {
|
||||||
background: `radial-gradient(
|
background: `radial-gradient(
|
||||||
86.12% 101.64% at 95.97% 94.07%,
|
86.12% 101.64% at 95.97% 94.07%,
|
||||||
@@ -131,55 +109,41 @@ const sharedCpus = 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-4">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row items-center justify-between">
|
<div class="flex flex-row flex-wrap items-center gap-3">
|
||||||
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
|
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
|
||||||
<div
|
<div
|
||||||
class="grid size-8 place-content-center rounded-full text-xs font-bold"
|
v-if="plans[plan].mostPopular"
|
||||||
:class="`${plans[plan].accentBg} ${plans[plan].accentText}`"
|
class="rounded-full bg-brand-highlight px-2 py-1 text-xs font-bold text-brand"
|
||||||
>
|
>
|
||||||
{{ formatMessage(plans[plan].symbol) }}
|
Most popular
|
||||||
</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">
|
||||||
${{ price / 100 }}<span class="text-lg font-semibold text-secondary">/month</span>
|
{{ formatPrice(locale, price / billingMonths, currency, true) }}
|
||||||
|
{{ 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="plan === 'medium' ? 'standard' : 'highlight-colored-text'"
|
:type="plans[plan].mostPopular ? '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')">
|
<button v-else @click="() => emit('select')">Select plan</button>
|
||||||
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 { usePyroFetch } from "~/composables/pyroFetch.ts";
|
import { useServersFetch } from "~/composables/servers/servers-fetch.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 usePyroFetch("notices").then((res) => {
|
await useServersFetch("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,9 +33,12 @@ 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 usePyroFetch(`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`, {
|
await useServersFetch(
|
||||||
method: "PUT",
|
`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`,
|
||||||
}).catch((err) => {
|
{
|
||||||
|
method: "PUT",
|
||||||
|
},
|
||||||
|
).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: "main",
|
||||||
title: "Error assigning notice",
|
title: "Error assigning notice",
|
||||||
@@ -75,9 +78,12 @@ 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 usePyroFetch(`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`, {
|
await useServersFetch(
|
||||||
method: "PUT",
|
`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`,
|
||||||
}).catch((err) => {
|
{
|
||||||
|
method: "PUT",
|
||||||
|
},
|
||||||
|
).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: "main",
|
||||||
title: "Error unassigning notice",
|
title: "Error unassigning notice",
|
||||||
|
|||||||
@@ -2,13 +2,8 @@
|
|||||||
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 { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||||
import { useRelativeTime } from "@modrinth/ui";
|
import { useRelativeTime, getDismissableMetadata, NOTICE_LEVELS } 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();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ 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) {
|
||||||
|
|||||||
@@ -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 is too large");
|
throw new Error("File exceeds the 1MiB size limit");
|
||||||
}
|
}
|
||||||
|
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
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
265
apps/frontend/src/composables/servers/modrinth-servers.ts
Normal file
265
apps/frontend/src/composables/servers/modrinth-servers.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
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 && iconUrl) {
|
||||||
|
// Handle external icon processing
|
||||||
|
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 (error) {
|
||||||
|
console.error("Failed to process external icon:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to process server icon:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
};
|
||||||
79
apps/frontend/src/composables/servers/modules/backups.ts
Normal file
79
apps/frontend/src/composables/servers/modules/backups.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
apps/frontend/src/composables/servers/modules/base.ts
Normal file
15
apps/frontend/src/composables/servers/modules/base.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
36
apps/frontend/src/composables/servers/modules/content.ts
Normal file
36
apps/frontend/src/composables/servers/modules/content.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
224
apps/frontend/src/composables/servers/modules/fs.ts
Normal file
224
apps/frontend/src/composables/servers/modules/fs.ts
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
173
apps/frontend/src/composables/servers/modules/general.ts
Normal file
173
apps/frontend/src/composables/servers/modules/general.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
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> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
apps/frontend/src/composables/servers/modules/index.ts
Normal file
8
apps/frontend/src/composables/servers/modules/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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";
|
||||||
47
apps/frontend/src/composables/servers/modules/network.ts
Normal file
47
apps/frontend/src/composables/servers/modules/network.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
26
apps/frontend/src/composables/servers/modules/startup.ts
Normal file
26
apps/frontend/src/composables/servers/modules/startup.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/frontend/src/composables/servers/modules/ws.ts
Normal file
13
apps/frontend/src/composables/servers/modules/ws.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
apps/frontend/src/composables/servers/servers-fetch.ts
Normal file
188
apps/frontend/src/composables/servers/servers-fetch.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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 amazing mods."
|
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with awazing 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,11 +350,14 @@
|
|||||||
"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."
|
||||||
@@ -365,12 +368,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"
|
||||||
},
|
},
|
||||||
@@ -1047,32 +1050,23 @@
|
|||||||
"message": "No notices"
|
"message": "No notices"
|
||||||
},
|
},
|
||||||
"servers.plan.large.description": {
|
"servers.plan.large.description": {
|
||||||
"message": "Ideal for larger communities, modpacks, and heavy modding."
|
"message": "Ideal for 15–25 players, modpacks, or 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 modded multiplayer and small communities."
|
"message": "Great for 6–15 players and multiple mods."
|
||||||
},
|
},
|
||||||
"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 vanilla multiplayer, small friend groups, SMPs, and light modding."
|
"message": "Perfect for 1–5 friends with a few light mods."
|
||||||
},
|
},
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ import { useVIntl } from "@vintl/vintl";
|
|||||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
|
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
|
||||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
|
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
@@ -290,7 +290,7 @@ const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>();
|
|||||||
await refreshNotices();
|
await refreshNotices();
|
||||||
|
|
||||||
async function refreshNotices() {
|
async function refreshNotices() {
|
||||||
await usePyroFetch("notices").then((res) => {
|
await useServersFetch("notices").then((res) => {
|
||||||
notices.value = res as ServerNoticeType[];
|
notices.value = res as ServerNoticeType[];
|
||||||
notices.value.sort((a, b) => {
|
notices.value.sort((a, b) => {
|
||||||
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at));
|
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at));
|
||||||
@@ -347,7 +347,7 @@ function startEditing(notice: ServerNoticeType, assignments: boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNotice(notice: ServerNoticeType) {
|
async function deleteNotice(notice: ServerNoticeType) {
|
||||||
await usePyroFetch(`notices/${notice.id}`, {
|
await useServersFetch(`notices/${notice.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -401,7 +401,7 @@ async function saveChanges() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await usePyroFetch(`notices/${editingNotice.value?.id}`, {
|
await useServersFetch(`notices/${editingNotice.value?.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
message: newNoticeMessage.value,
|
message: newNoticeMessage.value,
|
||||||
@@ -432,7 +432,7 @@ async function createNotice() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await usePyroFetch("notices", {
|
await useServersFetch("notices", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
message: newNoticeMessage.value,
|
message: newNoticeMessage.value,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<template v-if="orgs?.length > 0">
|
<template v-if="orgs?.length > 0">
|
||||||
<div class="orgs-grid">
|
<div class="orgs-grid">
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-for="org in orgs"
|
v-for="org in sortedOrgs"
|
||||||
:key="org.id"
|
:key="org.id"
|
||||||
:to="`/organization/${org.slug}`"
|
:to="`/organization/${org.slug}`"
|
||||||
class="universal-card button-base recessed org"
|
class="universal-card button-base recessed org"
|
||||||
@@ -67,6 +67,8 @@ const { data: orgs, error } = useAsyncData("organizations", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortedOrgs = computed(() => orgs.value.sort((a, b) => a.name.localeCompare(b.name)));
|
||||||
|
|
||||||
const onlyAcceptedMembers = (members) => members.filter((member) => member?.accepted);
|
const onlyAcceptedMembers = (members) => members.filter((member) => member?.accepted);
|
||||||
|
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ import {
|
|||||||
import Avatar from "~/components/ui/Avatar.vue";
|
import Avatar from "~/components/ui/Avatar.vue";
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
import Badge from "~/components/ui/Badge.vue";
|
||||||
import { formatProjectType } from "~/plugins/shorthands.js";
|
import { formatProjectType } from "~/plugins/shorthands.js";
|
||||||
|
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Review projects - Modrinth",
|
title: "Review projects - Modrinth",
|
||||||
@@ -170,28 +171,6 @@ const projectTypes = computed(() => {
|
|||||||
return [...set];
|
return [...set];
|
||||||
});
|
});
|
||||||
|
|
||||||
function segmentData(data, segmentSize = 800) {
|
|
||||||
return data.reduce((acc, curr, index) => {
|
|
||||||
const segment = Math.floor(index / segmentSize);
|
|
||||||
|
|
||||||
if (!acc[segment]) {
|
|
||||||
acc[segment] = [];
|
|
||||||
}
|
|
||||||
acc[segment].push(curr);
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchSegmented(data, createUrl, options = {}) {
|
|
||||||
return Promise.all(segmentData(data).map((ids) => useBaseFetch(createUrl(ids), options))).then(
|
|
||||||
(results) => results.flat(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function asEncodedJsonArray(data) {
|
|
||||||
return encodeURIComponent(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projects.value) {
|
if (projects.value) {
|
||||||
const teamIds = projects.value.map((x) => x.team_id);
|
const teamIds = projects.value.map((x) => x.team_id);
|
||||||
const orgIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
|
const orgIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ import {
|
|||||||
ImageIcon,
|
ImageIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||||
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
||||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
@@ -388,7 +389,7 @@ async function updateServerContext() {
|
|||||||
if (!auth.value.user) {
|
if (!auth.value.user) {
|
||||||
router.push("/auth/sign-in?redirect=" + encodeURIComponent(route.fullPath));
|
router.push("/auth/sign-in?redirect=" + encodeURIComponent(route.fullPath));
|
||||||
} else if (route.query.sid !== null) {
|
} else if (route.query.sid !== null) {
|
||||||
server.value = await usePyroServer(route.query.sid, ["general", "content"], {
|
server.value = await useModrinthServers(route.query.sid, ["general", "content"], {
|
||||||
waitForModules: true,
|
waitForModules: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,27 +4,28 @@
|
|||||||
data-pyro
|
data-pyro
|
||||||
class="servers-hero relative isolate -mt-44 h-full min-h-screen pt-8"
|
class="servers-hero relative isolate -mt-44 h-full min-h-screen pt-8"
|
||||||
>
|
>
|
||||||
<PurchaseModal
|
<ModrinthServersPurchaseModal
|
||||||
v-if="showModal && selectedProduct && customer"
|
v-if="customer"
|
||||||
:key="selectedProduct.id"
|
:key="`purchase-modal-${customer.id}`"
|
||||||
ref="purchaseModal"
|
ref="purchaseModal"
|
||||||
:product="selectedProduct"
|
|
||||||
:country="country"
|
|
||||||
:custom-server="customServer"
|
|
||||||
:publishable-key="config.public.stripePublishableKey"
|
:publishable-key="config.public.stripePublishableKey"
|
||||||
:send-billing-request="
|
:initiate-payment="
|
||||||
async (body) =>
|
async (body) =>
|
||||||
await useBaseFetch('billing/payment', { internal: true, method: 'POST', body })
|
await useBaseFetch('billing/payment', { internal: true, method: 'POST', body })
|
||||||
"
|
"
|
||||||
:fetch-payment-data="fetchPaymentData"
|
:available-products="pyroProducts"
|
||||||
:on-error="handleError"
|
:on-error="handleError"
|
||||||
:customer="customer"
|
:customer="customer"
|
||||||
:payment-methods="paymentMethods"
|
:payment-methods="paymentMethods"
|
||||||
|
:currency="selectedCurrency"
|
||||||
:return-url="`${config.public.siteUrl}/servers/manage`"
|
:return-url="`${config.public.siteUrl}/servers/manage`"
|
||||||
:server-name="`${auth?.user?.username}'s server`"
|
:server-name="`${auth?.user?.username}'s server`"
|
||||||
:fetch-capacity-statuses="fetchCapacityStatuses"
|
|
||||||
:out-of-stock-url="outOfStockUrl"
|
:out-of-stock-url="outOfStockUrl"
|
||||||
@hidden="handleModalHidden"
|
:fetch-capacity-statuses="fetchCapacityStatuses"
|
||||||
|
:pings="regionPings"
|
||||||
|
:regions="regions"
|
||||||
|
:refresh-payment-methods="fetchPaymentData"
|
||||||
|
:fetch-stock="fetchStock"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -442,8 +443,8 @@
|
|||||||
Where are Modrinth Servers located? Can I choose a region?
|
Where are Modrinth Servers located? Can I choose a region?
|
||||||
</summary>
|
</summary>
|
||||||
<p class="m-0 ml-6 leading-[160%]">
|
<p class="m-0 ml-6 leading-[160%]">
|
||||||
Currently, Modrinth Servers are located on the east coast of the United States in
|
We have servers in both North America in Vint Hill, Virginia, and Europe in Limburg,
|
||||||
Vint Hill, Virginia. More regions to come in the future!
|
Germany. More regions to come in the future!
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -497,98 +498,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
|
||||||
v-if="false"
|
|
||||||
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
|
||||||
>
|
|
||||||
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
|
||||||
<div class="mx-auto flex w-full max-w-7xl flex-col gap-8">
|
|
||||||
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
|
|
||||||
<div class="flex flex-col gap-8">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
class="relative w-fit rounded-full bg-highlight-green px-3 py-1 text-sm font-bold text-brand backdrop-blur-lg"
|
|
||||||
>
|
|
||||||
Server Locations
|
|
||||||
</div>
|
|
||||||
<h1 class="relative m-0 max-w-2xl text-4xl leading-[120%] md:text-7xl">
|
|
||||||
Coast-to-Coast Coverage
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-8">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="grid size-8 place-content-center rounded-full bg-highlight-green">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="text-brand"
|
|
||||||
>
|
|
||||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
|
|
||||||
<circle cx="12" cy="10" r="3" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
|
|
||||||
US Coverage
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
|
||||||
>
|
|
||||||
With strategically placed servers in New York, California, Texas, Florida, and
|
|
||||||
Washington, we ensure low latency connections for players across North America.
|
|
||||||
Each location is equipped with high-performance hardware and DDoS protection.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="grid size-8 place-content-center rounded-full bg-highlight-blue">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="text-blue"
|
|
||||||
>
|
|
||||||
<path d="M12 2a10 10 0 1 0 10 10" />
|
|
||||||
<path d="M18 13a6 6 0 0 0-6-6" />
|
|
||||||
<path d="M13 2.05a10 10 0 0 1 2 2" />
|
|
||||||
<path d="M19.5 8.5a10 10 0 0 1 2 2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
|
|
||||||
Global Expansion
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
|
||||||
>
|
|
||||||
We're expanding to Europe and Asia-Pacific regions soon, bringing Modrinth's
|
|
||||||
seamless hosting experience worldwide. Join our Discord to stay updated on new
|
|
||||||
region launches.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Globe />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id="plan"
|
id="plan"
|
||||||
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
||||||
@@ -596,19 +505,35 @@
|
|||||||
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
||||||
<div class="mx-auto flex w-full max-w-7xl flex-col items-center gap-8 text-center">
|
<div class="mx-auto flex w-full max-w-7xl flex-col items-center gap-8 text-center">
|
||||||
<h1 class="relative m-0 text-4xl leading-[120%] md:text-7xl">
|
<h1 class="relative m-0 text-4xl leading-[120%] md:text-7xl">
|
||||||
Start your server on Modrinth
|
There's a server for everyone
|
||||||
</h1>
|
</h1>
|
||||||
<h2
|
<p class="m-0 flex items-center gap-1">
|
||||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
Available in North America and Europe for wide coverage.
|
||||||
>
|
</p>
|
||||||
{{
|
|
||||||
isAtCapacity && !loggedOut
|
|
||||||
? "We are currently at capacity. Please try again later."
|
|
||||||
: "There's a plan for everyone! Choose the one that fits your needs."
|
|
||||||
}}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<ul class="m-0 mt-8 flex w-full flex-col gap-8 p-0 lg:flex-row">
|
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3">
|
||||||
|
<span></span>
|
||||||
|
<OptionGroup v-slot="{ option }" v-model="billingPeriod" :options="billingPeriods">
|
||||||
|
<template v-if="option === 'monthly'"> Pay monthly </template>
|
||||||
|
<span v-else-if="option === 'quarterly'"> Pay quarterly </span>
|
||||||
|
<span v-else-if="option === 'yearly'"> Pay yearly </span>
|
||||||
|
</OptionGroup>
|
||||||
|
<template v-if="billingPeriods.includes('quarterly')">
|
||||||
|
<button
|
||||||
|
v-if="billingPeriod !== 'quarterly'"
|
||||||
|
class="bg-transparent p-0 text-sm font-medium text-brand hover:underline active:scale-95"
|
||||||
|
@click="billingPeriod = 'quarterly'"
|
||||||
|
>
|
||||||
|
Save 16% with quarterly billing!
|
||||||
|
</button>
|
||||||
|
<span v-else class="bg-transparent p-0 text-sm font-medium text-brand">
|
||||||
|
Save 16% with quarterly billing!
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="m-0 flex w-full grid-cols-3 flex-col gap-8 p-0 lg:grid">
|
||||||
<ServerPlanSelector
|
<ServerPlanSelector
|
||||||
:capacity="capacityStatuses?.small?.available"
|
:capacity="capacityStatuses?.small?.available"
|
||||||
plan="small"
|
plan="small"
|
||||||
@@ -616,9 +541,12 @@
|
|||||||
:storage="plans.small.metadata.storage"
|
:storage="plans.small.metadata.storage"
|
||||||
:cpus="plans.small.metadata.cpu"
|
:cpus="plans.small.metadata.cpu"
|
||||||
:price="
|
:price="
|
||||||
plans.small?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
plans.small?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||||
?.monthly
|
?.intervals?.[billingPeriod]
|
||||||
"
|
"
|
||||||
|
:interval="billingPeriod"
|
||||||
|
:currency="selectedCurrency"
|
||||||
|
:is-usa="country.toLowerCase() === 'us'"
|
||||||
@select="selectProduct('small')"
|
@select="selectProduct('small')"
|
||||||
@scroll-to-faq="scrollToFaq()"
|
@scroll-to-faq="scrollToFaq()"
|
||||||
/>
|
/>
|
||||||
@@ -629,9 +557,12 @@
|
|||||||
:storage="plans.medium.metadata.storage"
|
:storage="plans.medium.metadata.storage"
|
||||||
:cpus="plans.medium.metadata.cpu"
|
:cpus="plans.medium.metadata.cpu"
|
||||||
:price="
|
:price="
|
||||||
plans.medium?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
plans.medium?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||||
?.monthly
|
?.intervals?.[billingPeriod]
|
||||||
"
|
"
|
||||||
|
:interval="billingPeriod"
|
||||||
|
:currency="selectedCurrency"
|
||||||
|
:is-usa="country.toLowerCase() === 'us'"
|
||||||
@select="selectProduct('medium')"
|
@select="selectProduct('medium')"
|
||||||
@scroll-to-faq="scrollToFaq()"
|
@scroll-to-faq="scrollToFaq()"
|
||||||
/>
|
/>
|
||||||
@@ -641,10 +572,13 @@
|
|||||||
:storage="plans.large.metadata.storage"
|
:storage="plans.large.metadata.storage"
|
||||||
:cpus="plans.large.metadata.cpu"
|
:cpus="plans.large.metadata.cpu"
|
||||||
:price="
|
:price="
|
||||||
plans.large?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
plans.large?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||||
?.monthly
|
?.intervals?.[billingPeriod]
|
||||||
"
|
"
|
||||||
|
:currency="selectedCurrency"
|
||||||
|
:is-usa="country.toLowerCase() === 'us'"
|
||||||
plan="large"
|
plan="large"
|
||||||
|
:interval="billingPeriod"
|
||||||
@select="selectProduct('large')"
|
@select="selectProduct('large')"
|
||||||
@scroll-to-faq="scrollToFaq()"
|
@scroll-to-faq="scrollToFaq()"
|
||||||
/>
|
/>
|
||||||
@@ -654,10 +588,9 @@
|
|||||||
class="mb-24 flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0"
|
class="mb-24 flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<h1 class="m-0">Build your own</h1>
|
<h1 class="m-0">Know exactly what you need?</h1>
|
||||||
<h2 class="m-0 text-base font-normal text-primary">
|
<h2 class="m-0 text-base font-normal text-primary">
|
||||||
If you're a more technical server administrator, you can pick your own RAM and storage
|
Pick a customized plan with just the specs you need.
|
||||||
options.
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -666,7 +599,7 @@
|
|||||||
>
|
>
|
||||||
<ButtonStyled color="standard" size="large">
|
<ButtonStyled color="standard" size="large">
|
||||||
<button class="w-full md:w-fit" @click="selectProduct('custom')">
|
<button class="w-full md:w-fit" @click="selectProduct('custom')">
|
||||||
Build your own
|
Get started
|
||||||
<RightArrowIcon class="shrink-0" />
|
<RightArrowIcon class="shrink-0" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -679,7 +612,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ButtonStyled, PurchaseModal } from "@modrinth/ui";
|
import { ButtonStyled, ModrinthServersPurchaseModal } from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
GameIcon,
|
GameIcon,
|
||||||
@@ -690,9 +623,13 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { products } from "~/generated/state.json";
|
import { products } from "~/generated/state.json";
|
||||||
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
||||||
import Globe from "~/components/ui/servers/Globe.vue";
|
|
||||||
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
||||||
|
import OptionGroup from "~/components/ui/OptionGroup.vue";
|
||||||
|
|
||||||
|
const billingPeriods = ref(["monthly", "quarterly"]);
|
||||||
|
const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly");
|
||||||
|
|
||||||
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
|
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
|
||||||
const pyroPlanProducts = pyroProducts.filter(
|
const pyroPlanProducts = pyroProducts.filter(
|
||||||
@@ -711,16 +648,6 @@ useSeoMeta({
|
|||||||
ogDescription: description,
|
ogDescription: description,
|
||||||
});
|
});
|
||||||
|
|
||||||
useHead({
|
|
||||||
script: [
|
|
||||||
{
|
|
||||||
src: "https://js.stripe.com/v3/",
|
|
||||||
defer: true,
|
|
||||||
async: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
@@ -740,6 +667,7 @@ const isDeleting = ref(false);
|
|||||||
const typingSpeed = 75;
|
const typingSpeed = 75;
|
||||||
const deletingSpeed = 25;
|
const deletingSpeed = 25;
|
||||||
const pauseTime = 2000;
|
const pauseTime = 2000;
|
||||||
|
const selectedCurrency = ref("USD");
|
||||||
|
|
||||||
const loggedOut = computed(() => !auth.value.user);
|
const loggedOut = computed(() => !auth.value.user);
|
||||||
const outOfStockUrl = "https://discord.modrinth.com";
|
const outOfStockUrl = "https://discord.modrinth.com";
|
||||||
@@ -747,13 +675,23 @@ const outOfStockUrl = "https://discord.modrinth.com";
|
|||||||
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
|
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
|
||||||
try {
|
try {
|
||||||
if (!auth.value.user) return false;
|
if (!auth.value.user) return false;
|
||||||
const response = await usePyroFetch("servers");
|
const response = await useServersFetch("servers");
|
||||||
return response.servers && response.servers.length > 0;
|
return response.servers && response.servers.length > 0;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function fetchStock(region, request) {
|
||||||
|
return useServersFetch(`stock?region=${region.shortcode}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
...request,
|
||||||
|
},
|
||||||
|
bypassAuth: true,
|
||||||
|
}).then((res) => res.available);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchCapacityStatuses(customProduct = null) {
|
async function fetchCapacityStatuses(customProduct = null) {
|
||||||
try {
|
try {
|
||||||
const productsToCheck = customProduct?.metadata
|
const productsToCheck = customProduct?.metadata
|
||||||
@@ -765,7 +703,7 @@ async function fetchCapacityStatuses(customProduct = null) {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
const capacityChecks = productsToCheck.map((product) =>
|
const capacityChecks = productsToCheck.map((product) =>
|
||||||
usePyroFetch("stock", {
|
useServersFetch("stock", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
cpu: product.metadata.cpu,
|
cpu: product.metadata.cpu,
|
||||||
@@ -841,23 +779,6 @@ const handleError = (err) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModalHidden = () => {
|
|
||||||
showModal.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(selectedProduct, async (newProduct) => {
|
|
||||||
if (newProduct) {
|
|
||||||
showModal.value = false;
|
|
||||||
await nextTick();
|
|
||||||
showModal.value = true;
|
|
||||||
modalKey.value++;
|
|
||||||
await nextTick();
|
|
||||||
if (purchaseModal.value && purchaseModal.value.show) {
|
|
||||||
purchaseModal.value.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchPaymentData() {
|
async function fetchPaymentData() {
|
||||||
if (!auth.value.user) return;
|
if (!auth.value.user) return;
|
||||||
try {
|
try {
|
||||||
@@ -954,8 +875,10 @@ const selectProduct = async (product) => {
|
|||||||
modalKey.value++;
|
modalKey.value++;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
if (purchaseModal.value && purchaseModal.value.show) {
|
if (product === "custom") {
|
||||||
purchaseModal.value.show();
|
purchaseModal.value?.show(billingPeriod.value);
|
||||||
|
} else {
|
||||||
|
purchaseModal.value?.show(billingPeriod.value, selectedProduct.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -966,9 +889,82 @@ const planQuery = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const regions = ref([]);
|
||||||
|
const regionPings = ref([]);
|
||||||
|
|
||||||
|
function pingRegions() {
|
||||||
|
useServersFetch("regions", {
|
||||||
|
method: "GET",
|
||||||
|
version: 1,
|
||||||
|
bypassAuth: true,
|
||||||
|
}).then((res) => {
|
||||||
|
regions.value = res;
|
||||||
|
regions.value.forEach((region) => {
|
||||||
|
runPingTest(region);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const PING_COUNT = 20;
|
||||||
|
const PING_INTERVAL = 200;
|
||||||
|
const MAX_PING_TIME = 1000;
|
||||||
|
|
||||||
|
function runPingTest(region, index = 1) {
|
||||||
|
if (index > 10) {
|
||||||
|
regionPings.value.push({
|
||||||
|
region: region.shortcode,
|
||||||
|
ping: -1,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = `wss://${region.shortcode}${index}.${region.zone}/pingtest`;
|
||||||
|
try {
|
||||||
|
const socket = new WebSocket(wsUrl);
|
||||||
|
const pings = [];
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
for (let i = 0; i < PING_COUNT; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.send(performance.now());
|
||||||
|
}, i * PING_INTERVAL);
|
||||||
|
}
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
socket.close();
|
||||||
|
|
||||||
|
const median = Math.round([...pings].sort((a, b) => a - b)[Math.floor(pings.length / 2)]);
|
||||||
|
if (median) {
|
||||||
|
regionPings.value.push({
|
||||||
|
region: region.shortcode,
|
||||||
|
ping: median,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PING_COUNT * PING_INTERVAL + MAX_PING_TIME,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
pings.push(performance.now() - event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (event) => {
|
||||||
|
console.error(
|
||||||
|
`Failed to connect pingtest WebSocket with ${wsUrl}, trying index ${index + 1}:`,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
runPingTest(region, index + 1);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to connect pingtest WebSocket with ${wsUrl}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startTyping();
|
startTyping();
|
||||||
planQuery();
|
planQuery();
|
||||||
|
pingRegions();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(customer, (newCustomer) => {
|
watch(customer, (newCustomer) => {
|
||||||
|
|||||||
@@ -63,8 +63,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="
|
v-else-if="
|
||||||
server.general?.error?.error.statusCode === 403 ||
|
server.moduleErrors?.general?.error.statusCode === 403 ||
|
||||||
server.general?.error?.error.statusCode === 404
|
server.moduleErrors?.general?.error.statusCode === 404
|
||||||
"
|
"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
is an error, please contact Modrinth Support.
|
is an error, please contact Modrinth Support.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
|
||||||
|
|
||||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
|
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
|
||||||
<button class="mt-6 !w-full">Go back to all servers</button>
|
<button class="mt-6 !w-full">Go back to all servers</button>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="server.general?.error?.error.statusCode === 503"
|
v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="server.general?.error"
|
v-else-if="server.moduleErrors?.general?.error"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
temporary network issue. You'll be reconnected automatically.
|
temporary network issue. You'll be reconnected automatically.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
:disabled="formattedTime !== '00'"
|
:disabled="formattedTime !== '00'"
|
||||||
size="large"
|
size="large"
|
||||||
@@ -207,6 +207,7 @@
|
|||||||
class="server-action-buttons-anim flex w-fit flex-shrink-0"
|
class="server-action-buttons-anim flex w-fit flex-shrink-0"
|
||||||
>
|
>
|
||||||
<UiServersPanelServerActionButton
|
<UiServersPanelServerActionButton
|
||||||
|
v-if="!serverData.flows?.intro"
|
||||||
class="flex-shrink-0"
|
class="flex-shrink-0"
|
||||||
:is-online="isServerRunning"
|
:is-online="isServerRunning"
|
||||||
:is-actioning="isActioning"
|
:is-actioning="isActioning"
|
||||||
@@ -220,7 +221,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="serverData.flows?.intro"
|
||||||
|
class="flex items-center gap-2 font-semibold text-secondary"
|
||||||
|
>
|
||||||
|
<SettingsIcon /> Configuring server...
|
||||||
|
</div>
|
||||||
<UiServersServerInfoLabels
|
<UiServersServerInfoLabels
|
||||||
|
v-else
|
||||||
:server-data="serverData"
|
:server-data="serverData"
|
||||||
:show-game-label="showGameLabel"
|
:show-game-label="showGameLabel"
|
||||||
:show-loader-label="showLoaderLabel"
|
:show-loader-label="showLoaderLabel"
|
||||||
@@ -231,149 +239,189 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<template v-if="serverData.flows?.intro">
|
||||||
data-pyro-navigation
|
|
||||||
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
|
||||||
>
|
|
||||||
<UiNavTabs :links="navLinks" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-pyro-mount class="h-full w-full flex-1">
|
|
||||||
<div
|
<div
|
||||||
v-if="error"
|
v-if="serverData?.status === 'installing'"
|
||||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
class="w-50 h-50 flex items-center justify-center gap-2 text-center text-lg font-bold"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row gap-4">
|
<LazyUiServersPanelSpinner class="size-10 animate-spin" /> Setting up your server...
|
||||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
</div>
|
||||||
<div class="flex flex-col gap-2 leading-[150%]">
|
<div v-else>
|
||||||
<div class="flex items-center gap-3">
|
<h2 class="my-4 text-xl font-extrabold">
|
||||||
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
What would you like to install on your new server?
|
||||||
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
</h2>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="errorTitle.toLocaleLowerCase() === 'installation error'" class="font-normal">
|
<ServerInstallation
|
||||||
<div
|
:server="server as ModrinthServer"
|
||||||
v-if="errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'"
|
:backup-in-progress="backupInProgress"
|
||||||
>
|
ignore-current-installation
|
||||||
An invalid loader or Minecraft version was specified and could not be installed.
|
@reinstall="onReinstall"
|
||||||
<ul class="m-0 mt-4 p-0 pl-4">
|
/>
|
||||||
<li>
|
</div>
|
||||||
If this version of Minecraft was released recently, please check if Modrinth
|
</template>
|
||||||
Servers supports it.
|
|
||||||
</li>
|
<template v-else>
|
||||||
<li>
|
<div
|
||||||
If you've installed a modpack, it may have been packaged incorrectly or may not
|
data-pyro-navigation
|
||||||
be compatible with the loader.
|
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
||||||
</li>
|
>
|
||||||
<li>
|
<UiNavTabs :links="navLinks" />
|
||||||
Your server may need to be reinstalled with a valid mod loader and version. You
|
</div>
|
||||||
can change the loader by clicking the "Change Loader" button.
|
|
||||||
</li>
|
<div data-pyro-mount class="h-full w-full flex-1">
|
||||||
<li>
|
<div
|
||||||
If you're stuck, please contact Modrinth Support with the information below:
|
v-if="error"
|
||||||
</li>
|
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||||
</ul>
|
>
|
||||||
<ButtonStyled>
|
<div class="flex flex-row gap-4">
|
||||||
<button class="mt-2" @click="copyServerDebugInfo">
|
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||||
<CopyIcon v-if="!copied" />
|
<div class="flex flex-col gap-2 leading-[150%]">
|
||||||
<CheckIcon v-else />
|
<div class="flex items-center gap-3">
|
||||||
Copy Debug Info
|
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
||||||
</button>
|
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
|
||||||
An internal error occurred while installing your server. Don't fret — try
|
|
||||||
reinstalling your server, and if the problem persists, please contact Modrinth
|
|
||||||
support with your server's debug information.
|
|
||||||
</div>
|
|
||||||
<div v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'">
|
|
||||||
An error occurred while installing your server because Modrinth Servers does not
|
|
||||||
support the version of Minecraft or the loader you specified. Try reinstalling your
|
|
||||||
server with a different version or loader, and if the problem persists, please
|
|
||||||
contact Modrinth Support with your server's debug information.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="errorTitle === 'Installation error'"
|
v-if="errorTitle.toLocaleLowerCase() === 'installation error'"
|
||||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
class="font-normal"
|
||||||
>
|
>
|
||||||
<ButtonStyled v-if="errorLog">
|
<div
|
||||||
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
v-if="
|
||||||
</ButtonStyled>
|
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
|
||||||
<ButtonStyled>
|
"
|
||||||
<button @click="copyServerDebugInfo">
|
>
|
||||||
<CopyIcon v-if="!copied" />
|
An invalid loader or Minecraft version was specified and could not be installed.
|
||||||
<CheckIcon v-else />
|
<ul class="m-0 mt-4 p-0 pl-4">
|
||||||
Copy Debug Info
|
<li>
|
||||||
</button>
|
If this version of Minecraft was released recently, please check if Modrinth
|
||||||
</ButtonStyled>
|
Servers supports it.
|
||||||
<ButtonStyled color="red" type="standard">
|
</li>
|
||||||
<NuxtLink
|
<li>
|
||||||
class="whitespace-pre"
|
If you've installed a modpack, it may have been packaged incorrectly or may
|
||||||
:to="`/servers/manage/${serverId}/options/loader`"
|
not be compatible with the loader.
|
||||||
>
|
</li>
|
||||||
<RightArrowIcon />
|
<li>
|
||||||
Change Loader
|
Your server may need to be reinstalled with a valid mod loader and version.
|
||||||
</NuxtLink>
|
You can change the loader by clicking the "Change Loader" button.
|
||||||
</ButtonStyled>
|
</li>
|
||||||
|
<li>
|
||||||
|
If you're stuck, please contact Modrinth Support with the information below:
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button class="mt-2" @click="copyServerDebugInfo">
|
||||||
|
<CopyIcon v-if="!copied" />
|
||||||
|
<CheckIcon v-else />
|
||||||
|
Copy Debug Info
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
||||||
|
An internal error occurred while installing your server. Don't fret — try
|
||||||
|
reinstalling your server, and if the problem persists, please contact Modrinth
|
||||||
|
support with your server's debug information.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
|
||||||
|
>
|
||||||
|
An error occurred while installing your server because Modrinth Servers does not
|
||||||
|
support the version of Minecraft or the loader you specified. Try reinstalling
|
||||||
|
your server with a different version or loader, and if the problem persists,
|
||||||
|
please contact Modrinth Support with your server's debug information.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="errorTitle === 'Installation error'"
|
||||||
|
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||||
|
>
|
||||||
|
<ButtonStyled v-if="errorLog">
|
||||||
|
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="copyServerDebugInfo">
|
||||||
|
<CopyIcon v-if="!copied" />
|
||||||
|
<CheckIcon v-else />
|
||||||
|
Copy Debug Info
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="red" type="standard">
|
||||||
|
<NuxtLink
|
||||||
|
class="whitespace-pre"
|
||||||
|
:to="`/servers/manage/${serverId}/options/loader`"
|
||||||
|
>
|
||||||
|
<RightArrowIcon />
|
||||||
|
Change Loader
|
||||||
|
</NuxtLink>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!isConnected && !isReconnecting && !isLoading"
|
v-if="!isConnected && !isReconnecting && !isLoading"
|
||||||
data-pyro-server-ws-error
|
data-pyro-server-ws-error
|
||||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
||||||
>
|
>
|
||||||
<IssuesIcon class="size-5 text-red" />
|
<IssuesIcon class="size-5 text-red" />
|
||||||
Something went wrong...
|
Something went wrong...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isReconnecting"
|
v-if="isReconnecting"
|
||||||
data-pyro-server-ws-reconnecting
|
data-pyro-server-ws-reconnecting
|
||||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
||||||
>
|
>
|
||||||
<UiServersPanelSpinner />
|
<UiServersPanelSpinner />
|
||||||
Hang on, we're reconnecting to your server.
|
Hang on, we're reconnecting to your server.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="serverData.status === 'installing'"
|
v-if="serverData.status === 'installing'"
|
||||||
data-pyro-server-installing
|
data-pyro-server-installing
|
||||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
||||||
>
|
>
|
||||||
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<span class="text-lg font-bold"> We're preparing your server! </span>
|
<span class="text-lg font-bold"> We're preparing your server! </span>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NuxtPage
|
||||||
|
:route="route"
|
||||||
|
:is-connected="isConnected"
|
||||||
|
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
||||||
|
:is-server-running="isServerRunning"
|
||||||
|
:stats="stats"
|
||||||
|
:server-power-state="serverPowerState"
|
||||||
|
:power-state-details="powerStateDetails"
|
||||||
|
:socket="socket"
|
||||||
|
:server="server"
|
||||||
|
:backup-in-progress="backupInProgress"
|
||||||
|
@reinstall="onReinstall"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NuxtPage
|
</template>
|
||||||
:route="route"
|
</div>
|
||||||
:is-connected="isConnected"
|
<div
|
||||||
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
v-if="flags.advancedDebugInfo"
|
||||||
:is-server-running="isServerRunning"
|
class="experimental-styles-within relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
|
||||||
:stats="stats"
|
>
|
||||||
:server-power-state="serverPowerState"
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
|
||||||
:power-state-details="powerStateDetails"
|
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
|
||||||
:socket="socket"
|
JSON.stringify(server, null, " ")
|
||||||
:server="server"
|
}}</pre>
|
||||||
:backup-in-progress="backupInProgress"
|
|
||||||
@reinstall="onReinstall"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
|
||||||
import {
|
import {
|
||||||
|
SettingsIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
LeftArrowIcon,
|
LeftArrowIcon,
|
||||||
@@ -386,12 +434,20 @@ import {
|
|||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { ButtonStyled, ServerNotice } from "@modrinth/ui";
|
import { ButtonStyled, ServerNotice } from "@modrinth/ui";
|
||||||
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
||||||
import { reloadNuxtApp, navigateTo } from "#app";
|
|
||||||
import type { MessageDescriptor } from "@vintl/vintl";
|
import type { MessageDescriptor } from "@vintl/vintl";
|
||||||
import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/types/servers";
|
import type {
|
||||||
import { usePyroConsole } from "~/store/console.ts";
|
ServerState,
|
||||||
import { type Backup } from "~/composables/pyroServers.ts";
|
Stats,
|
||||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
WSEvent,
|
||||||
|
WSInstallationResultEvent,
|
||||||
|
Backup,
|
||||||
|
PowerAction,
|
||||||
|
} from "@modrinth/utils";
|
||||||
|
import { reloadNuxtApp, navigateTo } from "#app";
|
||||||
|
import { useModrinthServersConsole } from "~/store/console.ts";
|
||||||
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
|
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
|
||||||
|
|
||||||
const app = useNuxtApp() as unknown as { $notify: any };
|
const app = useNuxtApp() as unknown as { $notify: any };
|
||||||
|
|
||||||
@@ -401,6 +457,7 @@ const isLoading = ref(true);
|
|||||||
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
||||||
const isFirstMount = ref(true);
|
const isFirstMount = ref(true);
|
||||||
const isMounted = ref(true);
|
const isMounted = ref(true);
|
||||||
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
const INTERCOM_APP_ID = ref("ykeritl9");
|
const INTERCOM_APP_ID = ref("ykeritl9");
|
||||||
const auth = (await useAuth()) as unknown as {
|
const auth = (await useAuth()) as unknown as {
|
||||||
@@ -417,19 +474,19 @@ const route = useNativeRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string;
|
||||||
|
|
||||||
const server = await usePyroServer(serverId, ["general", "ws"]);
|
const server: Reactive<ModrinthServer> = await useModrinthServers(serverId, ["general", "ws"]);
|
||||||
|
|
||||||
const loadModulesPromise = Promise.resolve().then(() => {
|
const loadModulesPromise = Promise.resolve().then(() => {
|
||||||
if (server.general?.status === "suspended") {
|
if (server.general?.status === "suspended") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return server.loadModules(["content", "backups", "network", "startup", "fs"]);
|
return server.refresh(["content", "backups", "network", "startup", "fs"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
provide("modulesLoaded", loadModulesPromise);
|
provide("modulesLoaded", loadModulesPromise);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [server.general?.error, server.ws?.error],
|
() => [server.moduleErrors?.general, server.moduleErrors?.ws],
|
||||||
([generalError, wsError]) => {
|
([generalError, wsError]) => {
|
||||||
if (server.general?.status === "suspended") return;
|
if (server.general?.status === "suspended") return;
|
||||||
|
|
||||||
@@ -447,7 +504,7 @@ const errorLogFile = ref("");
|
|||||||
const serverData = computed(() => server.general);
|
const serverData = computed(() => server.general);
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
const isWSAuthIncorrect = ref(false);
|
const isWSAuthIncorrect = ref(false);
|
||||||
const pyroConsole = usePyroConsole();
|
const modrinthServersConsole = useModrinthServersConsole();
|
||||||
const cpuData = ref<number[]>([]);
|
const cpuData = ref<number[]>([]);
|
||||||
const ramData = ref<number[]>([]);
|
const ramData = ref<number[]>([]);
|
||||||
const isActioning = ref(false);
|
const isActioning = ref(false);
|
||||||
@@ -621,7 +678,7 @@ const connectWebSocket = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pyroConsole.clear();
|
modrinthServersConsole.clear();
|
||||||
socket.value?.send(JSON.stringify({ event: "auth", jwt: wsAuth.value?.token }));
|
socket.value?.send(JSON.stringify({ event: "auth", jwt: wsAuth.value?.token }));
|
||||||
isConnected.value = true;
|
isConnected.value = true;
|
||||||
isReconnecting.value = false;
|
isReconnecting.value = false;
|
||||||
@@ -629,7 +686,7 @@ const connectWebSocket = () => {
|
|||||||
|
|
||||||
if (firstConnect.value) {
|
if (firstConnect.value) {
|
||||||
for (let i = 0; i < initialConsoleMessage.length; i++) {
|
for (let i = 0; i < initialConsoleMessage.length; i++) {
|
||||||
pyroConsole.addLine(initialConsoleMessage[i]);
|
modrinthServersConsole.addLine(initialConsoleMessage[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +709,9 @@ const connectWebSocket = () => {
|
|||||||
|
|
||||||
socket.value.onclose = () => {
|
socket.value.onclose = () => {
|
||||||
if (isMounted.value) {
|
if (isMounted.value) {
|
||||||
pyroConsole.addLine("\nSomething went wrong with the connection, we're reconnecting...");
|
modrinthServersConsole.addLine(
|
||||||
|
"\nSomething went wrong with the connection, we're reconnecting...",
|
||||||
|
);
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
scheduleReconnect();
|
scheduleReconnect();
|
||||||
}
|
}
|
||||||
@@ -710,7 +769,7 @@ const handleWebSocketMessage = (data: WSEvent) => {
|
|||||||
case "log":
|
case "log":
|
||||||
// eslint-disable-next-line no-case-declarations
|
// eslint-disable-next-line no-case-declarations
|
||||||
const log = data.message.split("\n").filter((l) => l.trim());
|
const log = data.message.split("\n").filter((l) => l.trim());
|
||||||
pyroConsole.addLines(log);
|
modrinthServersConsole.addLines(log);
|
||||||
break;
|
break;
|
||||||
case "stats":
|
case "stats":
|
||||||
updateStats(data);
|
updateStats(data);
|
||||||
@@ -812,6 +871,10 @@ const newLoaderVersion = ref<string | null>(null);
|
|||||||
const newMCVersion = ref<string | null>(null);
|
const newMCVersion = ref<string | null>(null);
|
||||||
|
|
||||||
const onReinstall = (potentialArgs: any) => {
|
const onReinstall = (potentialArgs: any) => {
|
||||||
|
if (serverData.value?.flows?.intro) {
|
||||||
|
server.general?.endIntro();
|
||||||
|
}
|
||||||
|
|
||||||
if (!serverData.value) return;
|
if (!serverData.value) return;
|
||||||
|
|
||||||
serverData.value.status = "installing";
|
serverData.value.status = "installing";
|
||||||
@@ -967,11 +1030,11 @@ const toAdverb = (word: string) => {
|
|||||||
return word + "ing";
|
return word + "ing";
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPowerAction = async (action: "restart" | "start" | "stop" | "kill") => {
|
const sendPowerAction = async (action: PowerAction) => {
|
||||||
const actionName = action.charAt(0).toUpperCase() + action.slice(1);
|
const actionName = action.charAt(0).toUpperCase() + action.slice(1);
|
||||||
try {
|
try {
|
||||||
isActioning.value = true;
|
isActioning.value = true;
|
||||||
await server.general?.power(actionName);
|
await server.general?.power(action);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error ${toAdverb(actionName)} server:`, error);
|
console.error(`Error ${toAdverb(actionName)} server:`, error);
|
||||||
notifyError(
|
notifyError(
|
||||||
@@ -1104,7 +1167,7 @@ const cleanup = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function dismissNotice(noticeId: number) {
|
async function dismissNotice(noticeId: number) {
|
||||||
await usePyroFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
|
await useServersFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
@@ -1123,14 +1186,18 @@ onMounted(() => {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (server.error) {
|
if (server.moduleErrors.general?.error) {
|
||||||
if (!server.error.message.includes("Forbidden")) {
|
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (server.general?.flows?.intro && server.general?.project) {
|
||||||
|
server.general?.endIntro();
|
||||||
|
}
|
||||||
|
|
||||||
if (username.value && email.value && userId.value && createdAt.value) {
|
if (username.value && email.value && userId.value && createdAt.value) {
|
||||||
const currentUser = auth.value?.user as any;
|
const currentUser = auth.value?.user as any;
|
||||||
const matches =
|
const matches =
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="server.backups?.error"
|
v-if="server.moduleErrors.backups"
|
||||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||||
@@ -15,7 +15,9 @@
|
|||||||
We couldn't load your server's backups. Here's what went wrong:
|
We couldn't load your server's backups. Here's what went wrong:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="break-all font-mono">{{ JSON.stringify(server.backups.error) }}</span>
|
<span class="break-all font-mono">{{
|
||||||
|
JSON.stringify(server.moduleErrors.backups.error)
|
||||||
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['backups'])">
|
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['backups'])">
|
||||||
<button class="mt-6 !w-full">Retry</button>
|
<button class="mt-6 !w-full">Retry</button>
|
||||||
@@ -152,16 +154,17 @@ import { ButtonStyled, TagItem } from "@modrinth/ui";
|
|||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
import { SpinnerIcon, PlusIcon, DownloadIcon, SettingsIcon, IssuesIcon } from "@modrinth/assets";
|
import { SpinnerIcon, PlusIcon, DownloadIcon, SettingsIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import type { Backup } from "@modrinth/utils";
|
||||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||||
import BackupRenameModal from "~/components/ui/servers/BackupRenameModal.vue";
|
import BackupRenameModal from "~/components/ui/servers/BackupRenameModal.vue";
|
||||||
import BackupCreateModal from "~/components/ui/servers/BackupCreateModal.vue";
|
import BackupCreateModal from "~/components/ui/servers/BackupCreateModal.vue";
|
||||||
import BackupRestoreModal from "~/components/ui/servers/BackupRestoreModal.vue";
|
import BackupRestoreModal from "~/components/ui/servers/BackupRestoreModal.vue";
|
||||||
import BackupDeleteModal from "~/components/ui/servers/BackupDeleteModal.vue";
|
import BackupDeleteModal from "~/components/ui/servers/BackupDeleteModal.vue";
|
||||||
import BackupSettingsModal from "~/components/ui/servers/BackupSettingsModal.vue";
|
import BackupSettingsModal from "~/components/ui/servers/BackupSettingsModal.vue";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
isServerRunning: boolean;
|
isServerRunning: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const data = computed(() => props.server.general);
|
const data = computed(() => props.server.general);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="server.content?.error"
|
v-if="server.moduleErrors.content"
|
||||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||||
@@ -24,7 +24,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-lg text-secondary">
|
<p class="text-lg text-secondary">
|
||||||
We couldn't load your server's {{ type.toLowerCase() }}s. Here's what we know:
|
We couldn't load your server's {{ type.toLowerCase() }}s. Here's what we know:
|
||||||
<span class="break-all font-mono">{{ JSON.stringify(server.content.error) }}</span>
|
<span class="break-all font-mono">{{
|
||||||
|
JSON.stringify(server.moduleErrors.content.error)
|
||||||
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['content'])">
|
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['content'])">
|
||||||
<button class="mt-6 !w-full">Retry</button>
|
<button class="mt-6 !w-full">Retry</button>
|
||||||
@@ -349,13 +351,14 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||||
|
import type { Mod } from "@modrinth/utils";
|
||||||
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
||||||
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
|
||||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const type = computed(() => {
|
const type = computed(() => {
|
||||||
|
|||||||
@@ -278,16 +278,11 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { ButtonStyled, ProgressBar } from "@modrinth/ui";
|
import { ButtonStyled, ProgressBar } from "@modrinth/ui";
|
||||||
import { formatBytes } from "@modrinth/utils";
|
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
import {
|
import type { FilesystemOp, FSQueuedOp, DirectoryItem, DirectoryResponse } from "@modrinth/utils";
|
||||||
type DirectoryResponse,
|
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
type DirectoryItem,
|
|
||||||
type Server,
|
|
||||||
handleError,
|
|
||||||
} from "~/composables/pyroServers.ts";
|
|
||||||
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
||||||
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
||||||
import type { FilesystemOp, FSQueuedOp } from "~/types/servers.ts";
|
|
||||||
import FilesUploadZipUrlModal from "~/components/ui/servers/FilesUploadZipUrlModal.vue";
|
import FilesUploadZipUrlModal from "~/components/ui/servers/FilesUploadZipUrlModal.vue";
|
||||||
import FilesUploadConflictModal from "~/components/ui/servers/FilesUploadConflictModal.vue";
|
import FilesUploadConflictModal from "~/components/ui/servers/FilesUploadConflictModal.vue";
|
||||||
|
|
||||||
@@ -316,7 +311,7 @@ interface RenameOperation extends BaseOperation {
|
|||||||
type Operation = MoveOperation | RenameOperation;
|
type Operation = MoveOperation | RenameOperation;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modulesLoaded = inject<Promise<void>>("modulesLoaded");
|
const modulesLoaded = inject<Promise<void>>("modulesLoaded");
|
||||||
@@ -402,7 +397,7 @@ const fetchDirectoryContents = async (): Promise<DirectoryResponse> => {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching directory contents:", error);
|
console.error("Error fetching directory contents:", error);
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 400) {
|
if (error instanceof ModrinthServersFetchError && error.statusCode === 400) {
|
||||||
return directoryData.value || { items: [], total: 0 };
|
return directoryData.value || { items: [], total: 0 };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -561,7 +556,7 @@ const handleRenameItem = async (newName: string) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error renaming item:", error);
|
console.error("Error renaming item:", error);
|
||||||
if (error instanceof PyroFetchError) {
|
if (error instanceof ModrinthServersFetchError) {
|
||||||
if (error.statusCode === 400) {
|
if (error.statusCode === 400) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "files",
|
group: "files",
|
||||||
@@ -719,7 +714,7 @@ const showDeleteModal = (item: any) => {
|
|||||||
|
|
||||||
const handleCreateError = (error: any) => {
|
const handleCreateError = (error: any) => {
|
||||||
console.error("Error creating item:", error);
|
console.error("Error creating item:", error);
|
||||||
if (error instanceof PyroFetchError) {
|
if (error instanceof ModrinthServersFetchError) {
|
||||||
if (error.statusCode === 400) {
|
if (error.statusCode === 400) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "files",
|
group: "files",
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="relative flex select-none flex-col gap-6" data-pyro-server-manager-root>
|
||||||
v-if="isConnected && !isWsAuthIncorrect"
|
|
||||||
class="relative flex select-none flex-col gap-6"
|
|
||||||
data-pyro-server-manager-root
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-if="inspectingError"
|
v-if="inspectingError && isConnected && !isWsAuthIncorrect"
|
||||||
data-pyro-servers-inspecting-error
|
data-pyro-servers-inspecting-error
|
||||||
class="flex justify-between rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
class="flex justify-between rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||||
>
|
>
|
||||||
@@ -77,26 +73,34 @@
|
|||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col-reverse gap-6 md:flex-col">
|
<div class="flex flex-col-reverse gap-6 md:flex-col">
|
||||||
<UiServersServerStats :data="stats" />
|
<UiServersServerStats
|
||||||
|
:data="isConnected && !isWsAuthIncorrect ? stats : undefined"
|
||||||
|
:loading="!isConnected || isWsAuthIncorrect"
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative flex h-[700px] w-full flex-col gap-3 overflow-hidden rounded-2xl border border-divider bg-bg-raised p-4 transition-all duration-300 ease-in-out md:p-8"
|
class="relative flex h-[700px] w-full flex-col gap-3 overflow-hidden rounded-2xl border border-divider bg-bg-raised p-4 transition-all duration-300 ease-in-out md:p-8"
|
||||||
|
:class="{ 'border-0': !isConnected || isWsAuthIncorrect }"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
|
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
|
||||||
|
<UiServersPanelServerStatus
|
||||||
<UiServersPanelServerStatus :state="serverPowerState" />
|
v-if="isConnected && !isWsAuthIncorrect"
|
||||||
|
:state="serverPowerState"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="flex flex-row items-center gap-2 text-sm font-medium">
|
|
||||||
<InfoIcon class="hidden sm:block" />
|
<UiServersPanelTerminal
|
||||||
Click and drag to select lines, then CMD+C to copy
|
:full-screen="fullScreen"
|
||||||
</div> -->
|
:loading="!isConnected || isWsAuthIncorrect"
|
||||||
<UiServersPanelTerminal :full-screen="fullScreen">
|
>
|
||||||
<div class="relative w-full px-4 pt-4">
|
<div class="relative w-full px-4 pt-4">
|
||||||
<ul
|
<ul
|
||||||
v-if="suggestions.length"
|
v-if="suggestions.length && isConnected && !isWsAuthIncorrect"
|
||||||
id="command-suggestions"
|
id="command-suggestions"
|
||||||
ref="suggestionsList"
|
ref="suggestionsList"
|
||||||
class="mt-1 max-h-60 w-full list-none overflow-auto rounded-md border border-divider bg-bg-raised p-0 shadow-lg"
|
class="mt-1 max-h-60 w-full list-none overflow-auto rounded-md border border-divider bg-bg-raised p-0 shadow-lg"
|
||||||
@@ -120,7 +124,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<span
|
<span
|
||||||
v-if="bestSuggestion"
|
v-if="bestSuggestion && isConnected && !isWsAuthIncorrect"
|
||||||
class="pointer-events-none absolute left-[26px] transform select-none text-gray-400"
|
class="pointer-events-none absolute left-[26px] transform select-none text-gray-400"
|
||||||
>
|
>
|
||||||
<span class="ml-[23.5px] whitespace-pre">{{
|
<span class="ml-[23.5px] whitespace-pre">{{
|
||||||
@@ -142,7 +146,7 @@
|
|||||||
<TerminalSquareIcon class="ml-3 h-5 w-5" />
|
<TerminalSquareIcon class="ml-3 h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="isServerRunning"
|
v-if="isServerRunning && isConnected && !isWsAuthIncorrect"
|
||||||
v-model="commandInput"
|
v-model="commandInput"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Send a command"
|
placeholder="Send a command"
|
||||||
@@ -168,29 +172,25 @@
|
|||||||
</UiServersPanelTerminal>
|
</UiServersPanelTerminal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<UiServersOverviewLoading v-else-if="!isConnected && !isWsAuthIncorrect" />
|
<div
|
||||||
<div v-else-if="isWsAuthIncorrect" class="flex flex-col">
|
v-if="isWsAuthIncorrect"
|
||||||
<h2>Could not connect to the server.</h2>
|
class="absolute inset-0 flex flex-col items-center justify-center bg-bg"
|
||||||
<p>
|
>
|
||||||
An error occurred while attempting to connect to your server. Please try refreshing the page.
|
<h2>Could not connect to the server.</h2>
|
||||||
(WebSocket Authentication Failed)
|
<p>
|
||||||
</p>
|
An error occurred while attempting to connect to your server. Please try refreshing the
|
||||||
</div>
|
page. (WebSocket Authentication Failed)
|
||||||
<div v-else class="flex flex-col">
|
</p>
|
||||||
<h2>Could not connect to the server.</h2>
|
</div>
|
||||||
<p>
|
|
||||||
An error occurred while attempting to connect to your server. Please try refreshing the page.
|
|
||||||
(No further information)
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TerminalSquareIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
import { TerminalSquareIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import type { ServerState, Stats } from "~/types/servers";
|
import type { ServerState, Stats } from "@modrinth/utils";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
type ServerProps = {
|
type ServerProps = {
|
||||||
socket: WebSocket | null;
|
socket: WebSocket | null;
|
||||||
@@ -203,7 +203,7 @@ type ServerProps = {
|
|||||||
exit_code?: number;
|
exit_code?: number;
|
||||||
};
|
};
|
||||||
isServerRunning: boolean;
|
isServerRunning: boolean;
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<ServerProps>();
|
const props = defineProps<ServerProps>();
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -114,11 +114,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { EditIcon, TransferIcon } from "@modrinth/assets";
|
import { EditIcon, TransferIcon } from "@modrinth/assets";
|
||||||
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const data = computed(() => props.server.general);
|
const data = computed(() => props.server.general);
|
||||||
|
|||||||
@@ -117,13 +117,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from "@modrinth/assets";
|
import { CopyIcon, ExternalIcon, EyeIcon, EyeOffIcon } from "@modrinth/assets";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const data = computed(() => props.server.general);
|
const data = computed(() => props.server.general);
|
||||||
|
|||||||
@@ -1,269 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<LazyUiServersPlatformVersionSelectModal
|
<ServerInstallation
|
||||||
ref="versionSelectModal"
|
|
||||||
:server="props.server"
|
:server="props.server"
|
||||||
:current-loader="data?.loader as Loaders"
|
:backup-in-progress="props.backupInProgress"
|
||||||
:backup-in-progress="backupInProgress"
|
|
||||||
@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')"
|
@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="data"
|
|
||||||
:is-installing="isInstalling"
|
|
||||||
@select-loader="selectLoader"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
|
||||||
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
|
||||||
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 ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
|
||||||
const { formatMessage } = useVIntl();
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reinstall: [any?];
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.stylized-toggle:checked::after {
|
|
||||||
background: var(--color-accent-contrast) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-base:active {
|
|
||||||
scale: none !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
|
|
||||||
<div class="relative h-full w-full overflow-y-auto">
|
<div class="relative h-full w-full overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-if="server.network?.error"
|
v-if="server.moduleErrors.network"
|
||||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||||
@@ -73,7 +73,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-lg text-secondary">
|
<p class="text-lg text-secondary">
|
||||||
We couldn't load your server's network settings. Here's what we know:
|
We couldn't load your server's network settings. Here's what we know:
|
||||||
<span class="break-all font-mono">{{ JSON.stringify(server.network.error) }}</span>
|
<span class="break-all font-mono">{{
|
||||||
|
JSON.stringify(server.moduleErrors.network.error)
|
||||||
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['network'])">
|
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['network'])">
|
||||||
<button class="mt-6 !w-full">Retry</button>
|
<button class="mt-6 !w-full">Retry</button>
|
||||||
@@ -273,10 +275,10 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { ButtonStyled, NewModal, ConfirmModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal, ConfirmModal } from "@modrinth/ui";
|
||||||
import { ref, computed, nextTick } from "vue";
|
import { ref, computed, nextTick } from "vue";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isUpdating = ref(false);
|
const isUpdating = ref(false);
|
||||||
|
|||||||
@@ -43,13 +43,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const preferences = {
|
const preferences = {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-full w-full select-none overflow-y-auto">
|
<div class="relative h-full w-full select-none overflow-y-auto">
|
||||||
<div v-if="server.fs?.error" class="flex w-full flex-col items-center justify-center gap-4 p-4">
|
<div
|
||||||
|
v-if="server.moduleErrors.fs"
|
||||||
|
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||||
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||||
<div class="flex flex-col items-center text-center">
|
<div class="flex flex-col items-center text-center">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
@@ -11,7 +14,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-lg text-secondary">
|
<p class="text-lg text-secondary">
|
||||||
We couldn't access your server's properties. Here's what we know:
|
We couldn't access your server's properties. Here's what we know:
|
||||||
<span class="break-all font-mono">{{ JSON.stringify(server.fs.error) }}</span>
|
<span class="break-all font-mono">{{
|
||||||
|
JSON.stringify(server.moduleErrors.fs.error)
|
||||||
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
|
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
|
||||||
<button class="mt-6 !w-full">Retry</button>
|
<button class="mt-6 !w-full">Retry</button>
|
||||||
@@ -141,10 +146,10 @@
|
|||||||
import { ref, watch, computed, inject } from "vue";
|
import { ref, watch, computed, inject } from "vue";
|
||||||
import { EyeIcon, SearchIcon, IssuesIcon } from "@modrinth/assets";
|
import { EyeIcon, SearchIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const tags = useTags();
|
const tags = useTags();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-full w-full">
|
<div class="relative h-full w-full">
|
||||||
<div
|
<div
|
||||||
v-if="server.startup?.error"
|
v-if="server.moduleErrors.startup"
|
||||||
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
class="flex w-full flex-col items-center justify-center gap-4 p-4"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
We couldn't load your server's startup settings. Here's what we know:
|
We couldn't load your server's startup settings. Here's what we know:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span class="break-all font-mono">{{ JSON.stringify(server.startup.error) }}</span>
|
<span class="break-all font-mono">{{
|
||||||
|
JSON.stringify(server.moduleErrors.startup.error)
|
||||||
|
}}</span>
|
||||||
</p>
|
</p>
|
||||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['startup'])">
|
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['startup'])">
|
||||||
<button class="mt-6 !w-full">Retry</button>
|
<button class="mt-6 !w-full">Retry</button>
|
||||||
@@ -112,10 +114,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { UpdatedIcon, IssuesIcon } from "@modrinth/assets";
|
import { UpdatedIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const data = computed(() => props.server.general);
|
const data = computed(() => props.server.general);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
data-pyro-server-list-root
|
data-pyro-server-list-root
|
||||||
class="experimental-styles-within relative mx-auto flex min-h-screen w-full max-w-[1280px] flex-col px-6"
|
class="experimental-styles-within relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="hasError || fetchError"
|
v-if="hasError || fetchError"
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<li v-if="fetchError" class="text-red">
|
<li v-if="fetchError" class="text-red">
|
||||||
<p>Error details:</p>
|
<p>Error details:</p>
|
||||||
<UiCopyCode
|
<UiCopyCode
|
||||||
:text="(fetchError as PyroFetchError).message || 'Unknown error'"
|
:text="(fetchError as ModrinthServersFetchError).message || 'Unknown error'"
|
||||||
:copyable="false"
|
:copyable="false"
|
||||||
:selectable="false"
|
:selectable="false"
|
||||||
:language="'json'"
|
:language="'json'"
|
||||||
@@ -89,7 +89,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-if="filteredData.length > 0" class="m-0 flex flex-col gap-4 p-0">
|
<ul
|
||||||
|
v-if="filteredData.length > 0 || isPollingForNewServers"
|
||||||
|
class="m-0 flex flex-col gap-4 p-0"
|
||||||
|
>
|
||||||
<UiServersServerListing
|
<UiServersServerListing
|
||||||
v-for="server in filteredData"
|
v-for="server in filteredData"
|
||||||
:key="server.server_id"
|
:key="server.server_id"
|
||||||
@@ -102,6 +105,7 @@
|
|||||||
:mc_version="server.mc_version"
|
:mc_version="server.mc_version"
|
||||||
:upstream="server.upstream"
|
:upstream="server.upstream"
|
||||||
:net="server.net"
|
:net="server.net"
|
||||||
|
:flows="server.flows"
|
||||||
/>
|
/>
|
||||||
<LazyUiServersServerListingSkeleton v-if="isPollingForNewServers" />
|
<LazyUiServersServerListingSkeleton v-if="isPollingForNewServers" />
|
||||||
</ul>
|
</ul>
|
||||||
@@ -117,9 +121,9 @@ import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
|||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { HammerIcon, PlusIcon, SearchIcon } from "@modrinth/assets";
|
import { HammerIcon, PlusIcon, SearchIcon } from "@modrinth/assets";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
|
import type { Server, ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
import { reloadNuxtApp } from "#app";
|
import { reloadNuxtApp } from "#app";
|
||||||
import type { PyroFetchError } from "~/composables/pyroFetch";
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import type { Server } from "~/types/servers";
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: "auth",
|
middleware: "auth",
|
||||||
@@ -133,6 +137,7 @@ interface ServerResponse {
|
|||||||
servers: Server[];
|
servers: Server[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const hasError = ref(false);
|
const hasError = ref(false);
|
||||||
const isPollingForNewServers = ref(false);
|
const isPollingForNewServers = ref(false);
|
||||||
@@ -141,7 +146,9 @@ const {
|
|||||||
data: serverResponse,
|
data: serverResponse,
|
||||||
error: fetchError,
|
error: fetchError,
|
||||||
refresh,
|
refresh,
|
||||||
} = await useAsyncData<ServerResponse>("ServerList", () => usePyroFetch<ServerResponse>("servers"));
|
} = await useAsyncData<ServerResponse>("ServerList", () =>
|
||||||
|
useServersFetch<ServerResponse>("servers"),
|
||||||
|
);
|
||||||
|
|
||||||
watch([fetchError, serverResponse], ([error, response]) => {
|
watch([fetchError, serverResponse], ([error, response]) => {
|
||||||
hasError.value = !!error || !response;
|
hasError.value = !!error || !response;
|
||||||
@@ -163,11 +170,19 @@ const fuse = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function introToTop(array: Server[]): Server[] {
|
||||||
|
return array.slice().sort((a, b) => {
|
||||||
|
return Number(b.flows?.intro) - Number(a.flows?.intro);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const filteredData = computed(() => {
|
const filteredData = computed(() => {
|
||||||
if (!searchInput.value.trim()) {
|
if (!searchInput.value.trim()) {
|
||||||
return serverList.value;
|
return introToTop(serverList.value);
|
||||||
}
|
}
|
||||||
return fuse.value ? fuse.value.search(searchInput.value).map((result) => result.item) : [];
|
return fuse.value
|
||||||
|
? introToTop(fuse.value.search(searchInput.value).map((result) => result.item))
|
||||||
|
: [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const previousServerList = ref<Server[]>([]);
|
const previousServerList = ref<Server[]>([]);
|
||||||
@@ -179,6 +194,7 @@ const checkForNewServers = async () => {
|
|||||||
if (JSON.stringify(previousServerList.value) !== JSON.stringify(serverList.value)) {
|
if (JSON.stringify(previousServerList.value) !== JSON.stringify(serverList.value)) {
|
||||||
isPollingForNewServers.value = false;
|
isPollingForNewServers.value = false;
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
|
router.replace({ query: {} });
|
||||||
} else if (refreshCount.value >= 5) {
|
} else if (refreshCount.value >= 5) {
|
||||||
isPollingForNewServers.value = false;
|
isPollingForNewServers.value = false;
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
|
|||||||
@@ -444,39 +444,13 @@
|
|||||||
:return-url="`${config.public.siteUrl}/servers/manage`"
|
:return-url="`${config.public.siteUrl}/servers/manage`"
|
||||||
:server-name="`${auth?.user?.username}'s server`"
|
:server-name="`${auth?.user?.username}'s server`"
|
||||||
/>
|
/>
|
||||||
<NewModal ref="addPaymentMethodModal">
|
<AddPaymentMethodModal
|
||||||
<template #title>
|
ref="addPaymentMethodModal"
|
||||||
<span class="text-lg font-extrabold text-contrast">
|
:publishable-key="config.public.stripePublishableKey"
|
||||||
{{ formatMessage(messages.paymentMethodTitle) }}
|
:return-url="`${config.public.siteUrl}/settings/billing`"
|
||||||
</span>
|
:create-setup-intent="createSetupIntent"
|
||||||
</template>
|
:on-error="handleError"
|
||||||
<div class="min-h-[16rem] md:w-[600px]">
|
/>
|
||||||
<div
|
|
||||||
v-show="loadingPaymentMethodModal !== 2"
|
|
||||||
class="flex min-h-[16rem] items-center justify-center"
|
|
||||||
>
|
|
||||||
<AnimatedLogo class="w-[80px]" />
|
|
||||||
</div>
|
|
||||||
<div v-show="loadingPaymentMethodModal === 2" class="min-h-[16rem] p-1">
|
|
||||||
<div id="address-element"></div>
|
|
||||||
<div id="payment-element" class="mt-4"></div>
|
|
||||||
</div>
|
|
||||||
<div v-show="loadingPaymentMethodModal === 2" class="input-group mt-auto pt-4">
|
|
||||||
<ButtonStyled color="brand">
|
|
||||||
<button :disabled="loadingAddMethod" @click="submit">
|
|
||||||
<PlusIcon />
|
|
||||||
{{ formatMessage(messages.paymentMethodAdd) }}
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled>
|
|
||||||
<button @click="$refs.addPaymentMethodModal.hide()">
|
|
||||||
<XIcon />
|
|
||||||
{{ formatMessage(commonMessages.cancelButton) }}
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NewModal>
|
|
||||||
<div class="header__row">
|
<div class="header__row">
|
||||||
<div class="header__title">
|
<div class="header__title">
|
||||||
<h2 class="text-2xl">{{ formatMessage(messages.paymentMethodTitle) }}</h2>
|
<h2 class="text-2xl">{{ formatMessage(messages.paymentMethodTitle) }}</h2>
|
||||||
@@ -590,9 +564,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
NewModal,
|
AddPaymentMethodModal,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
AnimatedLogo,
|
|
||||||
PurchaseModal,
|
PurchaseModal,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
CopyCode,
|
CopyCode,
|
||||||
@@ -617,8 +590,9 @@ import {
|
|||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
HistoryIcon,
|
HistoryIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { calculateSavings, formatPrice, createStripeElements, getCurrency } from "@modrinth/utils";
|
import { calculateSavings, formatPrice, getCurrency } from "@modrinth/utils";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import { products } from "~/generated/state.json";
|
import { products } from "~/generated/state.json";
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
@@ -754,19 +728,6 @@ const paymentMethodTypes = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let stripe = null;
|
|
||||||
let elements = null;
|
|
||||||
|
|
||||||
function loadStripe() {
|
|
||||||
try {
|
|
||||||
if (!stripe) {
|
|
||||||
stripe = Stripe(config.public.stripePublishableKey);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading Stripe:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ data: paymentMethods, refresh: refreshPaymentMethods },
|
{ data: paymentMethods, refresh: refreshPaymentMethods },
|
||||||
{ data: charges, refresh: refreshCharges },
|
{ data: charges, refresh: refreshCharges },
|
||||||
@@ -784,7 +745,7 @@ const [
|
|||||||
useBaseFetch("billing/subscriptions", { internal: true }),
|
useBaseFetch("billing/subscriptions", { internal: true }),
|
||||||
),
|
),
|
||||||
useAsyncData("billing/products", () => useBaseFetch("billing/products", { internal: true })),
|
useAsyncData("billing/products", () => useBaseFetch("billing/products", { internal: true })),
|
||||||
useAsyncData("servers", () => usePyroFetch("servers")),
|
useAsyncData("servers", () => useServersFetch("servers")),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const midasProduct = ref(products.find((x) => x.metadata?.type === "midas"));
|
const midasProduct = ref(products.find((x) => x.metadata?.type === "midas"));
|
||||||
@@ -842,69 +803,16 @@ const primaryPaymentMethodId = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const addPaymentMethodModal = ref();
|
const addPaymentMethodModal = ref();
|
||||||
const loadingPaymentMethodModal = ref(0);
|
|
||||||
async function addPaymentMethod() {
|
|
||||||
try {
|
|
||||||
loadingPaymentMethodModal.value = 0;
|
|
||||||
addPaymentMethodModal.value.show();
|
|
||||||
|
|
||||||
const result = await useBaseFetch("billing/payment_method", {
|
function addPaymentMethod() {
|
||||||
internal: true,
|
addPaymentMethodModal.value.show(paymentMethods.value);
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
loadStripe();
|
|
||||||
const {
|
|
||||||
elements: elementsVal,
|
|
||||||
addressElement,
|
|
||||||
paymentElement,
|
|
||||||
} = createStripeElements(stripe, paymentMethods.value, {
|
|
||||||
clientSecret: result.client_secret,
|
|
||||||
});
|
|
||||||
|
|
||||||
elements = elementsVal;
|
|
||||||
paymentElement.on("ready", () => {
|
|
||||||
loadingPaymentMethodModal.value += 1;
|
|
||||||
});
|
|
||||||
addressElement.on("ready", () => {
|
|
||||||
loadingPaymentMethodModal.value += 1;
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
data.$notify({
|
|
||||||
group: "main",
|
|
||||||
title: "An error occurred",
|
|
||||||
text: err.data ? err.data.description : err,
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingAddMethod = ref(false);
|
async function createSetupIntent() {
|
||||||
async function submit() {
|
return await useBaseFetch("billing/payment_method", {
|
||||||
startLoading();
|
internal: true,
|
||||||
loadingAddMethod.value = true;
|
method: "POST",
|
||||||
|
|
||||||
loadStripe();
|
|
||||||
const { error } = await stripe.confirmSetup({
|
|
||||||
elements,
|
|
||||||
confirmParams: {
|
|
||||||
return_url: `${config.public.siteUrl}/settings/billing`,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error && error.type !== "validation_error") {
|
|
||||||
data.$notify({
|
|
||||||
group: "main",
|
|
||||||
title: "An error occurred",
|
|
||||||
text: error.message,
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
} else if (!error) {
|
|
||||||
await refresh();
|
|
||||||
addPaymentMethodModal.value.close();
|
|
||||||
}
|
|
||||||
loadingAddMethod.value = false;
|
|
||||||
stopLoading();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removePaymentMethodIndex = ref();
|
const removePaymentMethodIndex = ref();
|
||||||
@@ -1077,7 +985,7 @@ async function fetchCapacityStatuses(serverId, product) {
|
|||||||
if (product) {
|
if (product) {
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
custom: await usePyroFetch(`servers/${serverId}/upgrade-stock`, {
|
custom: await useServersFetch(`servers/${serverId}/upgrade-stock`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
cpu: product.metadata.cpu,
|
cpu: product.metadata.cpu,
|
||||||
|
|||||||
@@ -303,7 +303,7 @@
|
|||||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
|
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-for="org in organizations"
|
v-for="org in sortedOrgs"
|
||||||
:key="org.id"
|
:key="org.id"
|
||||||
v-tooltip="org.name"
|
v-tooltip="org.name"
|
||||||
class="organization"
|
class="organization"
|
||||||
@@ -516,6 +516,8 @@ try {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortedOrgs = computed(() => organizations.value.sort((a, b) => a.name.localeCompare(b.name)));
|
||||||
|
|
||||||
if (!user.value) {
|
if (!user.value) {
|
||||||
throw createError({
|
throw createError({
|
||||||
fatal: true,
|
fatal: true,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user