Compare commits
29 Commits
cal/dev-64
...
alex/skin-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3587b0a9ce | ||
|
|
9dda6e0016 | ||
|
|
65d15fe751 | ||
|
|
8ecc7c5b86 | ||
|
|
a8226131d5 | ||
|
|
e189219407 | ||
|
|
f07bc86711 | ||
|
|
23f0c1dbf7 | ||
|
|
8abbc021ea | ||
|
|
ed8ff79809 | ||
|
|
f210de563d | ||
|
|
e410a07cac | ||
|
|
9f93cd8705 | ||
|
|
dd391be095 | ||
|
|
f84f8c1c2b | ||
|
|
301967d204 | ||
|
|
c9b98a6154 | ||
|
|
ab8e474339 | ||
|
|
8a26011e76 | ||
|
|
d4de1dc9a1 | ||
|
|
4e3bd4e282 | ||
|
|
d24528f6a6 | ||
|
|
1b1d41605b | ||
|
|
6955731def | ||
|
|
4386891716 | ||
|
|
6741aba880 | ||
|
|
ee8ee7af82 | ||
|
|
a2e323c9ee | ||
|
|
f8fb23e05f |
@@ -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)']
|
||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||
|
||||
[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)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||
rustflags: ''
|
||||
target: x86_64-apple-darwin
|
||||
|
||||
- name: Rust setup
|
||||
if: "!startsWith(matrix.platform, 'macos')"
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
rustflags: ''
|
||||
|
||||
- name: Setup rust cache
|
||||
uses: actions/cache@v4
|
||||
@@ -72,10 +72,10 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-rust-target-
|
||||
|
||||
- name: Use Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
|
||||
98
.github/workflows/turbo-ci.yml
vendored
98
.github/workflows/turbo-ci.yml
vendored
@@ -2,7 +2,7 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
@@ -10,71 +10,69 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build, Test, and Lint
|
||||
name: Lint and Test
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
# Ensure pnpm output is colored in GitHub Actions logs
|
||||
FORCE_COLOR: 3
|
||||
# Make cargo nextest successfully ignore projects without tests
|
||||
NEXTEST_NO_TESTS: pass
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install build dependencies
|
||||
- name: 🧰 Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Setup Node.JS environment
|
||||
- name: 🧰 Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🧰 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
- name: 🧰 Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
rustflags: ''
|
||||
components: clippy, rustfmt
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
- name: 🧰 Setup nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||
# back to a cached cargo install
|
||||
- name: 🧰 Setup cargo-sqlx
|
||||
uses: 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
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
- name: ⚙️ Start services
|
||||
run: docker compose up --wait
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
- name: ⚙️ Setup Labrinth environment and database
|
||||
working-directory: apps/labrinth
|
||||
run: |
|
||||
cp .env.local .env
|
||||
sqlx database setup
|
||||
|
||||
- name: Start docker compose
|
||||
run: docker compose up -d
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
||||
- name: 🔍 Lint and test
|
||||
run: pnpm run ci
|
||||
|
||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CommitMessageInspectionProfile">
|
||||
<profile version="1.0">
|
||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
|
||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -1379,6 +1379,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chardetng"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
@@ -1963,6 +1974,12 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.2"
|
||||
@@ -7978,6 +7995,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
@@ -8060,6 +8078,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"uuid 1.16.0",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@@ -8099,6 +8118,7 @@ dependencies = [
|
||||
"stringprep",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"uuid 1.16.0",
|
||||
"whoami",
|
||||
]
|
||||
|
||||
@@ -8125,6 +8145,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid 1.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8865,17 +8886,21 @@ dependencies = [
|
||||
"async_zip",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chardetng",
|
||||
"chrono",
|
||||
"daedalus",
|
||||
"dashmap",
|
||||
"data-url",
|
||||
"dirs",
|
||||
"discord-rich-presence",
|
||||
"dunce",
|
||||
"either",
|
||||
"encoding_rs",
|
||||
"enumset",
|
||||
"flate2",
|
||||
"fs4",
|
||||
"futures",
|
||||
"heck 0.5.0",
|
||||
"hickory-resolver",
|
||||
"indicatif",
|
||||
"notify",
|
||||
@@ -8890,6 +8915,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_ini",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"sha1_smol",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
|
||||
61
Cargo.toml
61
Cargo.toml
@@ -10,6 +10,9 @@ members = [
|
||||
"packages/daedalus",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
actix-cors = "0.7.1"
|
||||
actix-files = "0.6.6"
|
||||
@@ -21,6 +24,7 @@ actix-web-prom = "0.10.0"
|
||||
actix-ws = "0.3.0"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async_zip = "0.0.17"
|
||||
async-compression = { version = "0.4.24", default-features = false }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
@@ -31,11 +35,11 @@ async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
||||
"futures-03-sink",
|
||||
] }
|
||||
async-walkdir = "2.1.0"
|
||||
async_zip = "0.0.17"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.1"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.41"
|
||||
clap = "4.5.40"
|
||||
clickhouse = "0.13.3"
|
||||
@@ -43,6 +47,7 @@ color-thief = "0.2.2"
|
||||
console-subscriber = "0.4.1"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
dashmap = "6.1.0"
|
||||
data-url = "0.3.1"
|
||||
deadpool-redis = "0.21.1"
|
||||
dirs = "6.0.0"
|
||||
discord-rich-presence = "0.2.5"
|
||||
@@ -50,11 +55,13 @@ dotenv-build = "0.1.1"
|
||||
dotenvy = "0.15.7"
|
||||
dunce = "1.0.5"
|
||||
either = "1.15.0"
|
||||
encoding_rs = "0.8.35"
|
||||
enumset = "1.1.6"
|
||||
flate2 = "1.1.2"
|
||||
fs4 = { version = "0.13.1", default-features = false }
|
||||
futures = { version = "0.3.31", default-features = false }
|
||||
futures-util = "0.3.31"
|
||||
heck = "0.5.0"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25.2"
|
||||
hmac = "0.12.1"
|
||||
@@ -89,19 +96,19 @@ quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.37.5"
|
||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||
regex = "1.11.1"
|
||||
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 = [
|
||||
"serde-with-float",
|
||||
"serde-with-str",
|
||||
] }
|
||||
rust_iso3166 = "0.1.14"
|
||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||
"fail-on-err",
|
||||
"tags",
|
||||
"tokio-rustls-tls",
|
||||
] }
|
||||
rusty-money = "0.4.1"
|
||||
sentry = { version = "0.38.1", default-features = false, features = [
|
||||
"backtrace",
|
||||
@@ -113,12 +120,12 @@ sentry = { version = "0.38.1", default-features = false, features = [
|
||||
] }
|
||||
sentry-actix = "0.38.1"
|
||||
serde = "1.0.219"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
serde_bytes = "0.11.17"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_with = "3.12.0"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
@@ -168,6 +175,44 @@ zip = { version = "4.0.0", default-features = false, features = [
|
||||
] }
|
||||
zxcvbn = "3.1.0"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
bool_to_int_with_if = "warn"
|
||||
borrow_as_ptr = "warn"
|
||||
cfg_not_test = "warn"
|
||||
clear_with_drain = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
collection_is_never_read = "warn"
|
||||
dbg_macro = "warn"
|
||||
default_trait_access = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
filter_map_next = "warn"
|
||||
flat_map_option = "warn"
|
||||
format_push_string = "warn"
|
||||
get_unwrap = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
manual_assert = "warn"
|
||||
manual_instant_elapsed = "warn"
|
||||
manual_is_variant_and = "warn"
|
||||
manual_let_else = "warn"
|
||||
map_unwrap_or = "warn"
|
||||
match_bool = "warn"
|
||||
needless_collect = "warn"
|
||||
negative_feature_names = "warn"
|
||||
non_std_lazy_statics = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
read_zero_byte_vec = "warn"
|
||||
redundant_clone = "warn"
|
||||
redundant_feature_names = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
todo = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
|
||||
[workspace.lints.rust]
|
||||
# Turn warnings into errors by default
|
||||
warnings = "deny"
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
||||
|
||||
|
||||
@@ -9,18 +9,19 @@
|
||||
"tsc:check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"test": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
@@ -39,11 +40,12 @@
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@nuxt/eslint-config": "^0.5.6",
|
||||
"@taijased/vue-render-tracker": "^1.0.7",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"eslint-plugin-turbo": "^2.1.1",
|
||||
"eslint-plugin-turbo": "^2.5.4",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.74.1",
|
||||
@@ -51,8 +53,7 @@
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.6",
|
||||
"vue-tsc": "^2.1.6",
|
||||
"@taijased/vue-render-tracker": "^1.0.7"
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
"web-types": "../../web-types.json"
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
size="36px"
|
||||
:src="
|
||||
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'
|
||||
"
|
||||
/>
|
||||
<div class="flex flex-col w-full">
|
||||
<span>{{ selectedAccount ? selectedAccount.username : 'Select account' }}</span>
|
||||
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
||||
<span class="text-secondary text-xs">Minecraft account</span>
|
||||
</div>
|
||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||
@@ -28,12 +28,17 @@
|
||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||
>
|
||||
<div v-if="selectedAccount" class="selected account">
|
||||
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.id}/128`" />
|
||||
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`" />
|
||||
<div>
|
||||
<h4>{{ selectedAccount.username }}</h4>
|
||||
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||
<p>Selected</p>
|
||||
</div>
|
||||
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
|
||||
<Button
|
||||
v-tooltip="'Log out'"
|
||||
icon-only
|
||||
color="raised"
|
||||
@click="logout(selectedAccount.profile.id)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -44,12 +49,12 @@
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||
<div v-for="account in displayAccounts" :key="account.id" class="account-row">
|
||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||
<Button class="option account" @click="setAccount(account)">
|
||||
<Avatar :src="`https://mc-heads.net/avatar/${account.id}/128`" class="icon" />
|
||||
<p>{{ account.username }}</p>
|
||||
<Avatar :src="`https://mc-heads.net/avatar/${account.profile.id}/128`" class="icon" />
|
||||
<p>{{ account.profile.name }}</p>
|
||||
</Button>
|
||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
|
||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -101,16 +106,16 @@ defineExpose({
|
||||
await refreshValues()
|
||||
|
||||
const displayAccounts = computed(() =>
|
||||
accounts.value.filter((account) => defaultUser.value !== account.id),
|
||||
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||
)
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.id === defaultUser.value),
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
|
||||
async function setAccount(account) {
|
||||
defaultUser.value = account.id
|
||||
await set_default_user(account.id).catch(handleError)
|
||||
defaultUser.value = account.profile.id
|
||||
await set_default_user(account.profile.id).catch(handleError)
|
||||
emit('change')
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ async function loginMinecraft() {
|
||||
const loggedIn = await login_flow()
|
||||
|
||||
if (loggedIn) {
|
||||
await set_default_user(loggedIn.id).catch(handleError)
|
||||
await set_default_user(loggedIn.profile.id).catch(handleError)
|
||||
}
|
||||
|
||||
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
||||
|
||||
@@ -25,9 +25,8 @@ const editProfileObject = computed(() => {
|
||||
hooks?: Hooks
|
||||
} = {}
|
||||
|
||||
if (overrideHooks.value) {
|
||||
editProfile.hooks = hooks.value
|
||||
}
|
||||
// When hooks are not overridden per-instance, we want to clear them
|
||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
set_world_display_status,
|
||||
getWorldIdentifier,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||
import {
|
||||
useRelativeTime,
|
||||
@@ -49,6 +45,7 @@ const router = useRouter()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
(e: 'open-folder', world: SingleplayerWorld): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -380,8 +377,7 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'open-folder',
|
||||
shown: world.type === 'singleplayer',
|
||||
action: () =>
|
||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="instance.install_stage.includes('installing')"
|
||||
v-if="
|
||||
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
||||
instance.install_stage,
|
||||
)
|
||||
"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
||||
"
|
||||
@delete="() => promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,6 +152,7 @@ import {
|
||||
hasQuickPlaySupport,
|
||||
refreshWorlds,
|
||||
handleDefaultProfileUpdateEvent,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "theseus_playground"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -9,3 +9,6 @@ edition = "2024"
|
||||
theseus = { workspace = true, features = ["cli"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
enumset.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"name": "@modrinth/app-playground",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,10 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||
|
||||
let credentials = minecraft_auth::finish_login(&input, login).await?;
|
||||
|
||||
println!("Logged in user {}.", credentials.username);
|
||||
println!(
|
||||
"Logged in user {}.",
|
||||
credentials.maybe_online_profile().await.name
|
||||
);
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ version = "0.9.5"
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
edition.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { workspace = true, features = ["codegen"] }
|
||||
@@ -56,3 +55,6 @@ default = ["custom-protocol"]
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
updater = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -99,6 +99,22 @@ fn main() {
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"minecraft-skins",
|
||||
InlinedPlugin::new()
|
||||
.commands(&[
|
||||
"get_available_capes",
|
||||
"get_available_skins",
|
||||
"add_and_equip_custom_skin",
|
||||
"set_default_cape",
|
||||
"equip_skin",
|
||||
"remove_custom_skin",
|
||||
"unequip_skin",
|
||||
])
|
||||
.default_permission(
|
||||
DefaultPermissionRule::AllowAllCommands,
|
||||
),
|
||||
)
|
||||
.plugin(
|
||||
"mr-auth",
|
||||
InlinedPlugin::new()
|
||||
@@ -151,7 +167,6 @@ fn main() {
|
||||
"profile_update_managed_modrinth_version",
|
||||
"profile_repair_managed_modrinth",
|
||||
"profile_run",
|
||||
"profile_run_credentials",
|
||||
"profile_kill",
|
||||
"profile_edit",
|
||||
"profile_edit_icon",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"jre:default",
|
||||
"logs:default",
|
||||
"metadata:default",
|
||||
"minecraft-skins:default",
|
||||
"mr-auth:default",
|
||||
"profile-create:default",
|
||||
"pack:default",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@modrinth/app",
|
||||
"scripts": {
|
||||
"build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev",
|
||||
"test": "cargo test",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.5.0"
|
||||
|
||||
82
apps/app/src/api/minecraft_skins.rs
Normal file
82
apps/app/src/api/minecraft_skins.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use crate::api::Result;
|
||||
|
||||
use theseus::minecraft_skins::{self, Bytes, Cape, MinecraftSkinVariant, Skin};
|
||||
|
||||
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
tauri::plugin::Builder::new("minecraft-skins")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
add_and_equip_custom_skin,
|
||||
set_default_cape,
|
||||
equip_skin,
|
||||
remove_custom_skin,
|
||||
unequip_skin,
|
||||
])
|
||||
.build()
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_available_capes')`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_available_capes]
|
||||
#[tauri::command]
|
||||
pub async fn get_available_capes() -> Result<Vec<Cape>> {
|
||||
Ok(minecraft_skins::get_available_capes().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|get_available_skins')`
|
||||
///
|
||||
/// See also: [minecraft_skins::get_available_skins]
|
||||
#[tauri::command]
|
||||
pub async fn get_available_skins() -> Result<Vec<Skin>> {
|
||||
Ok(minecraft_skins::get_available_skins().await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|add_and_equip_custom_skin', texture_blob, variant, cape_override)`
|
||||
///
|
||||
/// See also: [minecraft_skins::add_and_equip_custom_skin]
|
||||
#[tauri::command]
|
||||
pub async fn add_and_equip_custom_skin(
|
||||
texture_blob: Bytes,
|
||||
variant: MinecraftSkinVariant,
|
||||
cape_override: Option<Cape>,
|
||||
) -> Result<()> {
|
||||
Ok(minecraft_skins::add_and_equip_custom_skin(
|
||||
texture_blob,
|
||||
variant,
|
||||
cape_override,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|set_default_cape', cape)`
|
||||
///
|
||||
/// See also: [minecraft_skins::set_default_cape]
|
||||
#[tauri::command]
|
||||
pub async fn set_default_cape(cape: Option<Cape>) -> Result<()> {
|
||||
Ok(minecraft_skins::set_default_cape(cape).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|equip_skin', skin)`
|
||||
///
|
||||
/// See also: [minecraft_skins::equip_skin]
|
||||
#[tauri::command]
|
||||
pub async fn equip_skin(skin: Skin) -> Result<()> {
|
||||
Ok(minecraft_skins::equip_skin(skin).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|remove_custom_skin', skin)`
|
||||
///
|
||||
/// See also: [minecraft_skins::remove_custom_skin]
|
||||
#[tauri::command]
|
||||
pub async fn remove_custom_skin(skin: Skin) -> Result<()> {
|
||||
Ok(minecraft_skins::remove_custom_skin(skin).await?)
|
||||
}
|
||||
|
||||
/// `invoke('plugin:minecraft-skins|unequip_skin')`
|
||||
///
|
||||
/// See also: [minecraft_skins::unequip_skin]
|
||||
#[tauri::command]
|
||||
pub async fn unequip_skin() -> Result<()> {
|
||||
Ok(minecraft_skins::unequip_skin().await?)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod import;
|
||||
pub mod jre;
|
||||
pub mod logs;
|
||||
pub mod metadata;
|
||||
pub mod minecraft_skins;
|
||||
pub mod mr_auth;
|
||||
pub mod pack;
|
||||
pub mod process;
|
||||
|
||||
@@ -28,7 +28,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
profile_update_managed_modrinth_version,
|
||||
profile_repair_managed_modrinth,
|
||||
profile_run,
|
||||
profile_run_credentials,
|
||||
profile_kill,
|
||||
profile_edit,
|
||||
profile_edit_icon,
|
||||
@@ -256,22 +255,6 @@ pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
// Run Minecraft using a profile using chosen credentials
|
||||
// Returns the UUID, which can be used to poll
|
||||
// for the actual Child in the state.
|
||||
// invoke('plugin:profile|profile_run_credentials', {path, credentials})')
|
||||
#[tauri::command]
|
||||
pub async fn profile_run_credentials(
|
||||
path: &str,
|
||||
credentials: Credentials,
|
||||
) -> Result<ProcessMetadata> {
|
||||
let process =
|
||||
profile::run_credentials(path, &credentials, &QuickPlayType::None)
|
||||
.await?;
|
||||
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn profile_kill(path: &str) -> Result<()> {
|
||||
profile::kill(path).await?;
|
||||
|
||||
@@ -37,6 +37,7 @@ pub fn get_os() -> OS {
|
||||
let os = OS::MacOS;
|
||||
os
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum OS {
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn get_recent_worlds<R: Runtime>(
|
||||
display_statuses.unwrap_or(EnumSet::all()),
|
||||
)
|
||||
.await?;
|
||||
for world in result.iter_mut() {
|
||||
for world in &mut result {
|
||||
adapt_world_icon(&app_handle, &mut world.world);
|
||||
}
|
||||
Ok(result)
|
||||
@@ -55,7 +55,7 @@ pub async fn get_profile_worlds<R: Runtime>(
|
||||
path: &str,
|
||||
) -> Result<Vec<World>> {
|
||||
let mut result = worlds::get_profile_worlds(path).await?;
|
||||
for world in result.iter_mut() {
|
||||
for world in &mut result {
|
||||
adapt_world_icon(&app_handle, world);
|
||||
}
|
||||
Ok(result)
|
||||
|
||||
@@ -11,7 +11,8 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
||||
manager: &M,
|
||||
) -> InitialPayload {
|
||||
let initial_payload = manager.try_state::<InitialPayload>();
|
||||
let mtx = if let Some(initial_payload) = initial_payload {
|
||||
|
||||
if let Some(initial_payload) = initial_payload {
|
||||
initial_payload.inner().clone()
|
||||
} else {
|
||||
tracing::info!("No initial payload found, creating new");
|
||||
@@ -22,7 +23,5 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
||||
manager.manage(payload.clone());
|
||||
|
||||
payload
|
||||
};
|
||||
|
||||
mtx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ fn main() {
|
||||
{
|
||||
let payload = macos::deep_link::get_or_init_payload(app);
|
||||
|
||||
let mtx_copy = payload.payload.clone();
|
||||
let mtx_copy = payload.payload;
|
||||
app.listen("deep-link://new-url", move |url| {
|
||||
let mtx_copy_copy = mtx_copy.clone();
|
||||
let request = url.payload().to_owned();
|
||||
@@ -229,7 +229,6 @@ fn main() {
|
||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||
payload,
|
||||
));
|
||||
dbg!(url);
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
@@ -249,6 +248,7 @@ fn main() {
|
||||
.plugin(api::logs::init())
|
||||
.plugin(api::jre::init())
|
||||
.plugin(api::metadata::init())
|
||||
.plugin(api::minecraft_skins::init())
|
||||
.plugin(api::pack::init())
|
||||
.plugin(api::process::init())
|
||||
.plugin(api::profile::init())
|
||||
@@ -273,22 +273,22 @@ fn main() {
|
||||
|
||||
match app {
|
||||
Ok(app) => {
|
||||
#[allow(unused_variables)]
|
||||
app.run(|app, event| {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
drop((app, event));
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Opened { urls } = event {
|
||||
tracing::info!("Handling webview open {urls:?}");
|
||||
|
||||
let file = urls
|
||||
.into_iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.next();
|
||||
.find_map(|url| url.to_file_path().ok());
|
||||
|
||||
if let Some(file) = file {
|
||||
let payload =
|
||||
macos::deep_link::get_or_init_payload(app);
|
||||
|
||||
let mtx_copy = payload.payload.clone();
|
||||
let mtx_copy = payload.payload;
|
||||
let request = file.to_string_lossy().to_string();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut payload = mtx_copy.lock().await;
|
||||
|
||||
14
apps/app/turbo.jsonc
Normal file
14
apps/app/turbo.jsonc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "../../node_modules/turbo/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
// Running Clippy and tests on a Tauri application requires
|
||||
// the frontend to be built at least once first
|
||||
"lint": {
|
||||
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "daedalus_client"
|
||||
version = "0.2.2"
|
||||
authors = ["Jai A <jai@modrinth.com>"]
|
||||
edition = "2024"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -28,3 +28,6 @@ tracing-error.workspace = true
|
||||
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.86.0 AS build
|
||||
FROM rust:1.87.0 AS build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/daedalus
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"name": "@modrinth/daedalus_client",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/daedalus": "workspace:*"
|
||||
|
||||
@@ -52,8 +52,7 @@ pub async fn fetch(
|
||||
if modrinth_version
|
||||
.original_sha1
|
||||
.as_ref()
|
||||
.map(|x| x == &version.sha1)
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|x| x == &version.sha1)
|
||||
{
|
||||
existing_versions.push(modrinth_version);
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"lint": "astro check",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
@@ -18,4 +19,4 @@
|
||||
"starlight-openapi": "^0.14.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"postinstall": "nuxi prepare",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"test": "nuxi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<script setup>
|
||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||
import { addReportMessage } from "~/helpers/threads.js";
|
||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||
|
||||
defineProps({
|
||||
moderation: {
|
||||
@@ -53,13 +54,13 @@ const threadIds = [
|
||||
|
||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||
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)}`, () =>
|
||||
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`),
|
||||
fetchSegmented(versionIds, (ids) => `versions?ids=${asEncodedJsonArray(ids)}`),
|
||||
),
|
||||
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 { 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) => {
|
||||
|
||||
@@ -45,9 +45,11 @@
|
||||
import { ref, nextTick, computed } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
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<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
@@ -64,7 +66,7 @@ const trimmedName = computed(() => backupName.value.trim());
|
||||
const nameExists = computed(() => {
|
||||
if (!props.server.backups?.data) return false;
|
||||
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();
|
||||
await props.server.refresh();
|
||||
} catch (error) {
|
||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||
isRateLimited.value = true;
|
||||
addNotification({
|
||||
type: "error",
|
||||
|
||||
@@ -20,13 +20,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
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";
|
||||
|
||||
defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete", backup: Backup | undefined): void;
|
||||
}>();
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||
import { ref } from "vue";
|
||||
import type { Backup } from "~/composables/pyroServers.ts";
|
||||
import type { Backup } from "@modrinth/utils";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
@@ -48,10 +48,11 @@
|
||||
import { ref, nextTick, computed } from "vue";
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
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<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
@@ -70,7 +71,7 @@ const nameExists = computed(() => {
|
||||
}
|
||||
|
||||
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">
|
||||
import { ref } from "vue";
|
||||
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 { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
|
||||
@@ -59,10 +59,10 @@
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { XIcon, SaveIcon } from "@modrinth/assets";
|
||||
import { ref, computed } from "vue";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["backups"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
|
||||
@@ -239,7 +239,7 @@ import {
|
||||
import { Admonition, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import TagItem from "@modrinth/ui/src/components/base/TagItem.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 Checkbox from "~/components/ui/Checkbox.vue";
|
||||
import ContentVersionFilter, {
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ref, computed, watch, nextTick } from "vue";
|
||||
import type { FSModule } from "~/composables/pyroServers.ts";
|
||||
import { FSModule } from "~/composables/servers/modules/fs.ts";
|
||||
|
||||
interface UploadItem {
|
||||
file: File;
|
||||
|
||||
@@ -75,13 +75,14 @@
|
||||
<script setup lang="ts">
|
||||
import { ExternalIcon, SpinnerIcon, DownloadIcon, XIcon } from "@modrinth/assets";
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||
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 props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const modal = ref<typeof NewModal>();
|
||||
@@ -110,24 +111,18 @@ const handleSubmit = async () => {
|
||||
if (!error.value) {
|
||||
// hide();
|
||||
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) {
|
||||
await props.server.fs?.extractFile(trimmedUrl.value, true, false, true);
|
||||
await props.server.fs.extractFile(trimmedUrl.value, true, false, true);
|
||||
hide();
|
||||
} else {
|
||||
submitted.value = false;
|
||||
handleError(
|
||||
new ServersError(
|
||||
new ModrinthServersFetchError(
|
||||
"Could not find CurseForge modpack at that URL.",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
context: "Error installing modpack",
|
||||
error: `url: ${url.value}`,
|
||||
description: "Could not find CurseForge modpack at that URL.",
|
||||
},
|
||||
404,
|
||||
new Error(`No modpack found at ${url.value}`),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<ButtonStyled v-if="showStopButton" type="transparent">
|
||||
<button :disabled="!canTakeAction" @click="initiateAction('stop')">
|
||||
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
||||
<div class="flex gap-1">
|
||||
<StopCircleIcon class="h-5 w-5" />
|
||||
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
||||
@@ -120,14 +120,12 @@ import {
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
type ServerAction = "start" | "stop" | "restart" | "kill";
|
||||
type ServerState = "stopped" | "starting" | "running" | "stopping" | "restarting";
|
||||
import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils";
|
||||
|
||||
const flags = useFeatureFlags();
|
||||
|
||||
interface PowerAction {
|
||||
action: ServerAction;
|
||||
action: ServerPowerAction;
|
||||
nextState: ServerState;
|
||||
}
|
||||
|
||||
@@ -142,7 +140,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action", action: ServerAction): void;
|
||||
(e: "action", action: ServerPowerAction): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
@@ -170,7 +168,7 @@ const isStoppingState = computed(() => serverState.value === "stopping");
|
||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
||||
|
||||
const primaryActionText = computed(() => {
|
||||
const states: Record<ServerState, string> = {
|
||||
const states: Partial<Record<ServerState, string>> = {
|
||||
starting: "Starting...",
|
||||
restarting: "Restarting...",
|
||||
running: "Restart",
|
||||
@@ -193,7 +191,7 @@ const menuOptions = computed(() => [
|
||||
id: "kill",
|
||||
label: "Kill server",
|
||||
icon: SlashIcon,
|
||||
action: () => initiateAction("kill"),
|
||||
action: () => initiateAction("Kill"),
|
||||
},
|
||||
]),
|
||||
{
|
||||
@@ -221,17 +219,17 @@ async function copyId() {
|
||||
await navigator.clipboard.writeText(serverId as string);
|
||||
}
|
||||
|
||||
function initiateAction(action: ServerAction) {
|
||||
function initiateAction(action: ServerPowerAction) {
|
||||
if (!canTakeAction.value) return;
|
||||
|
||||
const stateMap: Record<ServerAction, ServerState> = {
|
||||
start: "starting",
|
||||
stop: "stopping",
|
||||
restart: "restarting",
|
||||
kill: "stopping",
|
||||
const stateMap: Record<ServerPowerAction, ServerState> = {
|
||||
Start: "starting",
|
||||
Stop: "stopping",
|
||||
Restart: "restarting",
|
||||
Kill: "stopping",
|
||||
};
|
||||
|
||||
if (action === "start") {
|
||||
if (action === "Start") {
|
||||
emit("action", action);
|
||||
serverState.value = stateMap[action];
|
||||
startingDelay.value = true;
|
||||
@@ -249,7 +247,7 @@ function initiateAction(action: ServerAction) {
|
||||
}
|
||||
|
||||
function handlePrimaryAction() {
|
||||
initiateAction(isRunning.value ? "restart" : "start");
|
||||
initiateAction(isRunning.value ? "Restart" : "Start");
|
||||
}
|
||||
|
||||
function executePowerAction() {
|
||||
@@ -263,7 +261,7 @@ function executePowerAction() {
|
||||
userPreferences.value.powerDontAskAgain = true;
|
||||
}
|
||||
|
||||
if (action === "start") {
|
||||
if (action === "Start") {
|
||||
startingDelay.value = true;
|
||||
setTimeout(() => (startingDelay.value = false), 5000);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import type { ServerState } from "~/types/servers";
|
||||
import type { ServerState } from "@modrinth/utils";
|
||||
|
||||
const STATUS_CLASSES = {
|
||||
running: { main: "bg-brand", bg: "bg-bg-green" },
|
||||
@@ -49,7 +49,7 @@ const STATUS_CLASSES = {
|
||||
unknown: { main: "", bg: "" },
|
||||
} as const;
|
||||
|
||||
const STATUS_TEXTS = {
|
||||
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
|
||||
running: "Running",
|
||||
stopped: "",
|
||||
crashed: "Crashed",
|
||||
@@ -63,7 +63,10 @@ defineProps<{
|
||||
const isExpanded = ref(false);
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
type="text"
|
||||
placeholder="Search logs"
|
||||
class="h-12 !w-full !pl-10 !pr-48"
|
||||
:disabled="loading"
|
||||
@keydown.escape="clearSearch"
|
||||
/>
|
||||
<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">
|
||||
<XIcon class="h-5 w-5" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<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"
|
||||
>
|
||||
{{ pyroConsole.filteredOutput.value.length }}
|
||||
@@ -29,11 +30,13 @@
|
||||
: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',
|
||||
{ 'scale-fullscreen screen-fixed inset-0 z-50 !rounded-none': isFullScreen },
|
||||
{ 'pointer-events-none': loading },
|
||||
]"
|
||||
:aria-hidden="loading"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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"
|
||||
:style="`--transparency: ${Math.max(0, lerp(100, 0, bottomThreshold * 8))}%`"
|
||||
aria-hidden="true"
|
||||
@@ -47,7 +50,7 @@
|
||||
/>
|
||||
</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"
|
||||
:style="
|
||||
bottomThreshold > 0
|
||||
@@ -79,6 +82,7 @@
|
||||
</div>
|
||||
<div data-pyro-terminal-scroll-root class="relative h-full w-full">
|
||||
<div
|
||||
v-if="!loading"
|
||||
ref="scrollbarTrack"
|
||||
data-pyro-terminal-scrollbar-track
|
||||
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]"
|
||||
@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
|
||||
class="m-0 list-none p-0"
|
||||
data-pyro-terminal-virtual-list
|
||||
@@ -205,6 +214,7 @@
|
||||
<slot />
|
||||
</div>
|
||||
<button
|
||||
v-if="!loading"
|
||||
data-pyro-fullscreen
|
||||
: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"
|
||||
@@ -217,7 +227,7 @@
|
||||
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="hasSelection || isSingleLineSelected"
|
||||
v-if="(hasSelection || isSingleLineSelected) && !loading"
|
||||
class="absolute right-20 top-4 z-[3] flex flex-row items-center"
|
||||
:class="{ '!right-4': searchInput || hasSelection || isSingleLineSelected }"
|
||||
>
|
||||
@@ -247,7 +257,7 @@
|
||||
|
||||
<Transition name="scroll-to-bottom">
|
||||
<button
|
||||
v-if="bottomThreshold > 0 && !isScrolledToBottom"
|
||||
v-if="bottomThreshold > 0 && !isScrolledToBottom && !loading"
|
||||
data-pyro-scrolltobottom
|
||||
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"
|
||||
@@ -291,13 +301,14 @@ import { useDebounceFn } from "@vueuse/core";
|
||||
import { NewModal } from "@modrinth/ui";
|
||||
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
||||
import DOMPurify from "dompurify";
|
||||
import { usePyroConsole } from "~/store/console.ts";
|
||||
import { useModrinthServersConsole } from "~/store/console.ts";
|
||||
|
||||
const { $cosmetics } = useNuxtApp();
|
||||
const cosmetics = $cosmetics;
|
||||
|
||||
const props = defineProps<{
|
||||
fullScreen: boolean;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const BUFFER_SIZE = 5;
|
||||
@@ -307,8 +318,8 @@ const SEPARATOR_HEIGHT = 32;
|
||||
const SCROLL_END_DELAY = 150;
|
||||
const progressiveBlurIterations = ref(8);
|
||||
|
||||
const pyroConsole = usePyroConsole();
|
||||
const consoleOutput = pyroConsole.output;
|
||||
const pyroConsole = useModrinthServersConsole();
|
||||
const consoleOutput = computed(() => (props.loading ? [] : pyroConsole.output.value));
|
||||
|
||||
const scrollContainer = ref<HTMLElement | null>(null);
|
||||
|
||||
|
||||
@@ -69,10 +69,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
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<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
project: any;
|
||||
versions: any[];
|
||||
currentVersion?: any;
|
||||
@@ -98,8 +99,7 @@ const handleReinstall = async () => {
|
||||
try {
|
||||
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id;
|
||||
|
||||
await props.server.general?.reinstall(
|
||||
props.server.serverId,
|
||||
await props.server.general.reinstall(
|
||||
false,
|
||||
props.project.id,
|
||||
versionId,
|
||||
@@ -110,7 +110,7 @@ const handleReinstall = async () => {
|
||||
emit("reinstall");
|
||||
hide();
|
||||
} catch (error) {
|
||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Cannot reinstall server",
|
||||
|
||||
@@ -116,11 +116,12 @@
|
||||
<script setup lang="ts">
|
||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||
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 { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
@@ -175,7 +176,7 @@ const handleReinstall = async () => {
|
||||
window.scrollTo(0, 0);
|
||||
hide();
|
||||
} catch (error) {
|
||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Cannot reinstall server",
|
||||
|
||||
@@ -200,9 +200,9 @@
|
||||
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
||||
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
||||
import { $fetch } from "ofetch";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { Loaders } from "~/types/servers";
|
||||
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
@@ -220,7 +220,7 @@ type VersionMap = Record<string, LoaderVersion[]>;
|
||||
type VersionCache = Record<string, any>;
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
currentLoader: Loaders | undefined;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
initialSetup?: boolean;
|
||||
@@ -458,7 +458,6 @@ const handleReinstall = async () => {
|
||||
|
||||
try {
|
||||
await props.server.general?.reinstall(
|
||||
props.server.serverId,
|
||||
true,
|
||||
selectedLoader.value,
|
||||
selectedMCVersion.value,
|
||||
@@ -474,7 +473,7 @@ const handleReinstall = async () => {
|
||||
|
||||
hide();
|
||||
} catch (error) {
|
||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
||||
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
|
||||
addNotification({
|
||||
group: "server",
|
||||
title: "Cannot reinstall server",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
isUpdating: boolean;
|
||||
@@ -39,7 +39,7 @@ const props = defineProps<{
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
isVisible: boolean;
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const saveAndRestart = async () => {
|
||||
|
||||
@@ -160,14 +160,14 @@
|
||||
<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 { 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: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
ignoreCurrentInstallation?: boolean;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
@@ -81,12 +81,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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>>();
|
||||
|
||||
if (props.server_id) {
|
||||
await usePyroServer(props.server_id, ["general"]);
|
||||
await useModrinthServers(props.server_id, ["general"]);
|
||||
}
|
||||
|
||||
const showGameLabel = computed(() => !!props.game);
|
||||
@@ -109,7 +110,7 @@ if (props.upstream) {
|
||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
||||
|
||||
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);
|
||||
|
||||
@@ -34,15 +34,15 @@
|
||||
<script setup lang="ts">
|
||||
import { RightArrowIcon } from "@modrinth/assets";
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const emit = defineEmits(["reinstall"]);
|
||||
|
||||
defineProps<{
|
||||
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
|
||||
route: RouteLocationNormalized;
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
data-pyro-server-stats
|
||||
style="font-variant-numeric: tabular-nums"
|
||||
class="flex select-none flex-col items-center gap-6 md:flex-row"
|
||||
:class="{ 'pointer-events-none': loading }"
|
||||
:aria-hidden="loading"
|
||||
>
|
||||
<div
|
||||
v-for="(metric, index) in metrics"
|
||||
@@ -18,7 +20,7 @@
|
||||
<h3 class="flex items-center gap-2 text-base font-normal text-secondary">
|
||||
{{ metric.title }}
|
||||
<IssuesIcon
|
||||
v-if="metric.warning"
|
||||
v-if="metric.warning && !loading"
|
||||
v-tooltip="metric.warning"
|
||||
class="size-5"
|
||||
: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>
|
||||
|
||||
<component :is="metric.icon" class="absolute right-10 top-10 z-10" />
|
||||
<ClientOnly>
|
||||
<VueApexCharts
|
||||
v-if="metric.showGraph"
|
||||
type="area"
|
||||
height="142"
|
||||
:options="getChartOptions(metric.warning)"
|
||||
:series="[{ name: metric.title, data: metric.data }]"
|
||||
class="chart absolute bottom-0 left-0 right-0 w-full opacity-0"
|
||||
/>
|
||||
</ClientOnly>
|
||||
<component
|
||||
:is="metric.icon"
|
||||
class="absolute right-10 top-10 z-10 size-8"
|
||||
style="width: 2rem; height: 2rem"
|
||||
/>
|
||||
|
||||
<div class="chart-space absolute bottom-0 left-0 right-0">
|
||||
<ClientOnly>
|
||||
<VueApexCharts
|
||||
v-if="metric.showGraph && !loading"
|
||||
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>
|
||||
|
||||
<NuxtLink
|
||||
:to="`/servers/manage/${serverId}/files`"
|
||||
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100"
|
||||
<component
|
||||
:is="loading ? 'div' : 'NuxtLink'"
|
||||
: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">
|
||||
<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>
|
||||
</div>
|
||||
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
||||
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
||||
</NuxtLink>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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 type { Stats } from "~/types/servers";
|
||||
import type { Stats } from "@modrinth/utils";
|
||||
|
||||
const route = useNativeRoute();
|
||||
const serverId = route.params.id;
|
||||
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
||||
|
||||
const chartsReady = ref(new Set<number>());
|
||||
|
||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||
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 units = ["B", "KB", "MB", "GB"];
|
||||
@@ -94,6 +121,29 @@ const updateGraphData = (arr: number[], newValue: number) => {
|
||||
};
|
||||
|
||||
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(
|
||||
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
||||
100,
|
||||
@@ -119,7 +169,7 @@ const metrics = computed(() => {
|
||||
? formatBytes(stats.value.ram_usage_bytes)
|
||||
: `${ramPercent.toFixed(2)}%`,
|
||||
max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%",
|
||||
icon: DBIcon,
|
||||
icon: DatabaseIcon,
|
||||
data: ramData.value,
|
||||
showGraph: true,
|
||||
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: {
|
||||
type: "area",
|
||||
animations: { enabled: false },
|
||||
@@ -139,6 +189,10 @@ const getChartOptions = (hasWarning: string | null) => ({
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
events: {
|
||||
mounted: () => onChartReady(index),
|
||||
updated: () => onChartReady(index),
|
||||
},
|
||||
},
|
||||
stroke: { curve: "smooth", width: 3 },
|
||||
fill: {
|
||||
@@ -172,24 +226,26 @@ const getChartOptions = (hasWarning: string | null) => ({
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data.current,
|
||||
() => props.data?.current,
|
||||
(newStats) => {
|
||||
stats.value = newStats;
|
||||
if (newStats) {
|
||||
stats.value = newStats;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
animation: fadeIn 0.2s ease-out 0.2s forwards;
|
||||
.chart-space {
|
||||
height: 142px;
|
||||
width: calc(100% + 48px);
|
||||
margin-left: -24px;
|
||||
margin-right: -24px;
|
||||
width: calc(100% + 48px) !important;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
.chart {
|
||||
width: 100% !important;
|
||||
height: 142px !important;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LoaderIcon } from "@modrinth/assets";
|
||||
import type { Loaders } from "~/types/servers";
|
||||
import type { Loaders } from "@modrinth/utils";
|
||||
|
||||
defineProps<{
|
||||
loader: Loaders;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
||||
import type { MessageDescriptor } from "@vintl/vintl";
|
||||
import { formatPrice } from "../../../../../../../packages/utils";
|
||||
import { formatPrice } from "@modrinth/utils";
|
||||
|
||||
const { formatMessage, locale } = useVIntl();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modri
|
||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||
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 };
|
||||
|
||||
@@ -23,7 +23,7 @@ const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === "no
|
||||
const inputField = ref("");
|
||||
|
||||
async function refresh() {
|
||||
await usePyroFetch("notices").then((res) => {
|
||||
await useServersFetch("notices").then((res) => {
|
||||
const notices = res as ServerNoticeType[];
|
||||
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();
|
||||
|
||||
if (input !== "" && notice.value) {
|
||||
await usePyroFetch(`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`, {
|
||||
method: "PUT",
|
||||
}).catch((err) => {
|
||||
await useServersFetch(
|
||||
`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`,
|
||||
{
|
||||
method: "PUT",
|
||||
},
|
||||
).catch((err) => {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error assigning notice",
|
||||
@@ -75,9 +78,12 @@ async function unassignDetect() {
|
||||
|
||||
async function unassign(id: string, server: boolean = true) {
|
||||
if (notice.value) {
|
||||
await usePyroFetch(`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`, {
|
||||
method: "PUT",
|
||||
}).catch((err) => {
|
||||
await useServersFetch(
|
||||
`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
},
|
||||
).catch((err) => {
|
||||
app.$notify({
|
||||
group: "main",
|
||||
title: "Error unassigning notice",
|
||||
|
||||
@@ -2,13 +2,8 @@
|
||||
import dayjs from "dayjs";
|
||||
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
|
||||
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
|
||||
import { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||
import { useRelativeTime } from "@modrinth/ui";
|
||||
import {
|
||||
DISMISSABLE,
|
||||
getDismissableMetadata,
|
||||
NOTICE_LEVELS,
|
||||
} from "@modrinth/ui/src/utils/notices.ts";
|
||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||
import { useRelativeTime, getDismissableMetadata, NOTICE_LEVELS } from "@modrinth/ui";
|
||||
import { useVIntl } from "@vintl/vintl";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
|
||||
@@ -19,7 +19,7 @@ export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
|
||||
|
||||
// Make sure file is less than 1MB
|
||||
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();
|
||||
|
||||
@@ -1,112 +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(/^\//, "")}`
|
||||
: version === 0
|
||||
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
|
||||
: `${base}/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);
|
||||
}
|
||||
@@ -77,9 +77,6 @@ const errorMessages = computed(
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// TODO: REMOVE BEFORE MERGE
|
||||
console.log(props.error);
|
||||
|
||||
watch(route, () => {
|
||||
console.log(route);
|
||||
});
|
||||
|
||||
@@ -275,7 +275,7 @@ import { useVIntl } from "@vintl/vintl";
|
||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||
import { computed } from "vue";
|
||||
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";
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
@@ -290,7 +290,7 @@ const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>();
|
||||
await refreshNotices();
|
||||
|
||||
async function refreshNotices() {
|
||||
await usePyroFetch("notices").then((res) => {
|
||||
await useServersFetch("notices").then((res) => {
|
||||
notices.value = res as ServerNoticeType[];
|
||||
notices.value.sort((a, b) => {
|
||||
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) {
|
||||
await usePyroFetch(`notices/${notice.id}`, {
|
||||
await useServersFetch(`notices/${notice.id}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
.then(() => {
|
||||
@@ -401,7 +401,7 @@ async function saveChanges() {
|
||||
return;
|
||||
}
|
||||
|
||||
await usePyroFetch(`notices/${editingNotice.value?.id}`, {
|
||||
await useServersFetch(`notices/${editingNotice.value?.id}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
message: newNoticeMessage.value,
|
||||
@@ -432,7 +432,7 @@ async function createNotice() {
|
||||
return;
|
||||
}
|
||||
|
||||
await usePyroFetch("notices", {
|
||||
await useServersFetch("notices", {
|
||||
method: "POST",
|
||||
body: {
|
||||
message: newNoticeMessage.value,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<template v-if="orgs?.length > 0">
|
||||
<div class="orgs-grid">
|
||||
<nuxt-link
|
||||
v-for="org in orgs"
|
||||
v-for="org in sortedOrgs"
|
||||
:key="org.id"
|
||||
:to="`/organization/${org.slug}`"
|
||||
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);
|
||||
|
||||
if (error.value) {
|
||||
|
||||
@@ -115,6 +115,7 @@ import {
|
||||
import Avatar from "~/components/ui/Avatar.vue";
|
||||
import Badge from "~/components/ui/Badge.vue";
|
||||
import { formatProjectType } from "~/plugins/shorthands.js";
|
||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||
|
||||
useHead({
|
||||
title: "Review projects - Modrinth",
|
||||
@@ -170,28 +171,6 @@ const projectTypes = computed(() => {
|
||||
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) {
|
||||
const teamIds = projects.value.map((x) => x.team_id);
|
||||
const orgIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
|
||||
|
||||
@@ -334,6 +334,7 @@ import {
|
||||
ImageIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { computed } from "vue";
|
||||
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
@@ -388,7 +389,7 @@ async function updateServerContext() {
|
||||
if (!auth.value.user) {
|
||||
router.push("/auth/sign-in?redirect=" + encodeURIComponent(route.fullPath));
|
||||
} 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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -623,6 +623,7 @@ import {
|
||||
ServerIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { products } from "~/generated/state.json";
|
||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
||||
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
||||
import OptionGroup from "~/components/ui/OptionGroup.vue";
|
||||
@@ -674,7 +675,7 @@ const outOfStockUrl = "https://discord.modrinth.com";
|
||||
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
|
||||
try {
|
||||
if (!auth.value.user) return false;
|
||||
const response = await usePyroFetch("servers");
|
||||
const response = await useServersFetch("servers");
|
||||
return response.servers && response.servers.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -682,7 +683,7 @@ const { data: hasServers } = await useAsyncData("ServerListCountCheck", async ()
|
||||
});
|
||||
|
||||
function fetchStock(region, request) {
|
||||
return usePyroFetch(`stock?region=${region.shortcode}`, {
|
||||
return useServersFetch(`stock?region=${region.shortcode}`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
...request,
|
||||
@@ -702,7 +703,7 @@ async function fetchCapacityStatuses(customProduct = null) {
|
||||
),
|
||||
];
|
||||
const capacityChecks = productsToCheck.map((product) =>
|
||||
usePyroFetch("stock", {
|
||||
useServersFetch("stock", {
|
||||
method: "POST",
|
||||
body: {
|
||||
cpu: product.metadata.cpu,
|
||||
@@ -892,7 +893,7 @@ const regions = ref([]);
|
||||
const regionPings = ref([]);
|
||||
|
||||
function pingRegions() {
|
||||
usePyroFetch("regions", {
|
||||
useServersFetch("regions", {
|
||||
method: "GET",
|
||||
version: 1,
|
||||
bypassAuth: true,
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
server.general?.error?.error.statusCode === 403 ||
|
||||
server.general?.error?.error.statusCode === 404
|
||||
server.moduleErrors?.general?.error.statusCode === 403 ||
|
||||
server.moduleErrors?.general?.error.statusCode === 404
|
||||
"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
@@ -81,7 +81,7 @@
|
||||
is an error, please contact Modrinth Support.
|
||||
</p>
|
||||
</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')">
|
||||
<button class="mt-6 !w-full">Go back to all servers</button>
|
||||
@@ -89,7 +89,7 @@
|
||||
</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"
|
||||
>
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
||||
<UiCopyCode :text="JSON.stringify(server.moduleErrors?.general?.error)" />
|
||||
<ButtonStyled
|
||||
:disabled="formattedTime !== '00'"
|
||||
size="large"
|
||||
@@ -252,7 +252,7 @@
|
||||
</h2>
|
||||
|
||||
<ServerInstallation
|
||||
:server="server"
|
||||
:server="server as ModrinthServer"
|
||||
:backup-in-progress="backupInProgress"
|
||||
ignore-current-installation
|
||||
@reinstall="onReinstall"
|
||||
@@ -419,7 +419,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
|
||||
import {
|
||||
SettingsIcon,
|
||||
CopyIcon,
|
||||
@@ -434,12 +434,19 @@ import {
|
||||
import DOMPurify from "dompurify";
|
||||
import { ButtonStyled, ServerNotice } from "@modrinth/ui";
|
||||
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
||||
import { reloadNuxtApp, navigateTo } from "#app";
|
||||
import type { MessageDescriptor } from "@vintl/vintl";
|
||||
import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/types/servers";
|
||||
import { usePyroConsole } from "~/store/console.ts";
|
||||
import { type Backup } from "~/composables/pyroServers.ts";
|
||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
||||
import type {
|
||||
ServerState,
|
||||
Stats,
|
||||
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 };
|
||||
@@ -467,19 +474,19 @@ const route = useNativeRoute();
|
||||
const router = useRouter();
|
||||
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(() => {
|
||||
if (server.general?.status === "suspended") {
|
||||
return;
|
||||
}
|
||||
return server.loadModules(["content", "backups", "network", "startup", "fs"]);
|
||||
return server.refresh(["content", "backups", "network", "startup", "fs"]);
|
||||
});
|
||||
|
||||
provide("modulesLoaded", loadModulesPromise);
|
||||
|
||||
watch(
|
||||
() => [server.general?.error, server.ws?.error],
|
||||
() => [server.moduleErrors?.general, server.moduleErrors?.ws],
|
||||
([generalError, wsError]) => {
|
||||
if (server.general?.status === "suspended") return;
|
||||
|
||||
@@ -497,7 +504,7 @@ const errorLogFile = ref("");
|
||||
const serverData = computed(() => server.general);
|
||||
const isConnected = ref(false);
|
||||
const isWSAuthIncorrect = ref(false);
|
||||
const pyroConsole = usePyroConsole();
|
||||
const modrinthServersConsole = useModrinthServersConsole();
|
||||
const cpuData = ref<number[]>([]);
|
||||
const ramData = ref<number[]>([]);
|
||||
const isActioning = ref(false);
|
||||
@@ -671,7 +678,7 @@ const connectWebSocket = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
pyroConsole.clear();
|
||||
modrinthServersConsole.clear();
|
||||
socket.value?.send(JSON.stringify({ event: "auth", jwt: wsAuth.value?.token }));
|
||||
isConnected.value = true;
|
||||
isReconnecting.value = false;
|
||||
@@ -679,7 +686,7 @@ const connectWebSocket = () => {
|
||||
|
||||
if (firstConnect.value) {
|
||||
for (let i = 0; i < initialConsoleMessage.length; i++) {
|
||||
pyroConsole.addLine(initialConsoleMessage[i]);
|
||||
modrinthServersConsole.addLine(initialConsoleMessage[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,7 +709,9 @@ const connectWebSocket = () => {
|
||||
|
||||
socket.value.onclose = () => {
|
||||
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;
|
||||
scheduleReconnect();
|
||||
}
|
||||
@@ -760,7 +769,7 @@ const handleWebSocketMessage = (data: WSEvent) => {
|
||||
case "log":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const log = data.message.split("\n").filter((l) => l.trim());
|
||||
pyroConsole.addLines(log);
|
||||
modrinthServersConsole.addLines(log);
|
||||
break;
|
||||
case "stats":
|
||||
updateStats(data);
|
||||
@@ -1021,11 +1030,11 @@ const toAdverb = (word: string) => {
|
||||
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);
|
||||
try {
|
||||
isActioning.value = true;
|
||||
await server.general?.power(actionName);
|
||||
await server.general?.power(action);
|
||||
} catch (error) {
|
||||
console.error(`Error ${toAdverb(actionName)} server:`, error);
|
||||
notifyError(
|
||||
@@ -1158,7 +1167,7 @@ const cleanup = () => {
|
||||
};
|
||||
|
||||
async function dismissNotice(noticeId: number) {
|
||||
await usePyroFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
|
||||
await useServersFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
|
||||
method: "POST",
|
||||
}).catch((err) => {
|
||||
app.$notify({
|
||||
@@ -1177,8 +1186,8 @@ onMounted(() => {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
if (server.error) {
|
||||
if (!server.error.message.includes("Forbidden")) {
|
||||
if (server.moduleErrors.general?.error) {
|
||||
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
|
||||
startPolling();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<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:
|
||||
</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>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['backups'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
@@ -152,16 +154,17 @@ import { ButtonStyled, TagItem } from "@modrinth/ui";
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import { SpinnerIcon, PlusIcon, DownloadIcon, SettingsIcon, IssuesIcon } from "@modrinth/assets";
|
||||
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 BackupRenameModal from "~/components/ui/servers/BackupRenameModal.vue";
|
||||
import BackupCreateModal from "~/components/ui/servers/BackupCreateModal.vue";
|
||||
import BackupRestoreModal from "~/components/ui/servers/BackupRestoreModal.vue";
|
||||
import BackupDeleteModal from "~/components/ui/servers/BackupDeleteModal.vue";
|
||||
import BackupSettingsModal from "~/components/ui/servers/BackupSettingsModal.vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
isServerRunning: boolean;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const route = useNativeRoute();
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
/>
|
||||
|
||||
<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"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
@@ -24,7 +24,9 @@
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
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>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['content'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
@@ -349,13 +351,14 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||
import type { Mod } from "@modrinth/utils";
|
||||
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.vue";
|
||||
import FilesUploadDropdown from "~/components/ui/servers/FilesUploadDropdown.vue";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const type = computed(() => {
|
||||
|
||||
@@ -278,16 +278,11 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
import { computed } from "vue";
|
||||
import { ButtonStyled, ProgressBar } from "@modrinth/ui";
|
||||
import { formatBytes } from "@modrinth/utils";
|
||||
import {
|
||||
type DirectoryResponse,
|
||||
type DirectoryItem,
|
||||
type Server,
|
||||
handleError,
|
||||
} from "~/composables/pyroServers.ts";
|
||||
import { formatBytes, ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import type { FilesystemOp, FSQueuedOp, DirectoryItem, DirectoryResponse } from "@modrinth/utils";
|
||||
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
import FilesUploadDragAndDrop from "~/components/ui/servers/FilesUploadDragAndDrop.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 FilesUploadConflictModal from "~/components/ui/servers/FilesUploadConflictModal.vue";
|
||||
|
||||
@@ -316,7 +311,7 @@ interface RenameOperation extends BaseOperation {
|
||||
type Operation = MoveOperation | RenameOperation;
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const modulesLoaded = inject<Promise<void>>("modulesLoaded");
|
||||
@@ -402,7 +397,7 @@ const fetchDirectoryContents = async (): Promise<DirectoryResponse> => {
|
||||
};
|
||||
} catch (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 };
|
||||
}
|
||||
throw error;
|
||||
@@ -561,7 +556,7 @@ const handleRenameItem = async (newName: string) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error renaming item:", error);
|
||||
if (error instanceof PyroFetchError) {
|
||||
if (error instanceof ModrinthServersFetchError) {
|
||||
if (error.statusCode === 400) {
|
||||
addNotification({
|
||||
group: "files",
|
||||
@@ -719,7 +714,7 @@ const showDeleteModal = (item: any) => {
|
||||
|
||||
const handleCreateError = (error: any) => {
|
||||
console.error("Error creating item:", error);
|
||||
if (error instanceof PyroFetchError) {
|
||||
if (error instanceof ModrinthServersFetchError) {
|
||||
if (error.statusCode === 400) {
|
||||
addNotification({
|
||||
group: "files",
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isConnected && !isWsAuthIncorrect"
|
||||
class="relative flex select-none flex-col gap-6"
|
||||
data-pyro-server-manager-root
|
||||
>
|
||||
<div class="relative flex select-none flex-col gap-6" data-pyro-server-manager-root>
|
||||
<div
|
||||
v-if="inspectingError"
|
||||
v-if="inspectingError && isConnected && !isWsAuthIncorrect"
|
||||
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"
|
||||
>
|
||||
@@ -77,26 +73,34 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col-reverse gap-6 md:flex-col">
|
||||
<UiServersServerStats :data="stats" />
|
||||
<UiServersServerStats
|
||||
:data="isConnected && !isWsAuthIncorrect ? stats : undefined"
|
||||
:loading="!isConnected || isWsAuthIncorrect"
|
||||
/>
|
||||
|
||||
<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="{ 'border-0': !isConnected || isWsAuthIncorrect }"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
|
||||
|
||||
<UiServersPanelServerStatus :state="serverPowerState" />
|
||||
<UiServersPanelServerStatus
|
||||
v-if="isConnected && !isWsAuthIncorrect"
|
||||
:state="serverPowerState"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex flex-row items-center gap-2 text-sm font-medium">
|
||||
<InfoIcon class="hidden sm:block" />
|
||||
Click and drag to select lines, then CMD+C to copy
|
||||
</div> -->
|
||||
<UiServersPanelTerminal :full-screen="fullScreen">
|
||||
|
||||
<UiServersPanelTerminal
|
||||
:full-screen="fullScreen"
|
||||
:loading="!isConnected || isWsAuthIncorrect"
|
||||
>
|
||||
<div class="relative w-full px-4 pt-4">
|
||||
<ul
|
||||
v-if="suggestions.length"
|
||||
v-if="suggestions.length && isConnected && !isWsAuthIncorrect"
|
||||
id="command-suggestions"
|
||||
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"
|
||||
@@ -120,7 +124,7 @@
|
||||
</ul>
|
||||
<div class="relative flex items-center">
|
||||
<span
|
||||
v-if="bestSuggestion"
|
||||
v-if="bestSuggestion && isConnected && !isWsAuthIncorrect"
|
||||
class="pointer-events-none absolute left-[26px] transform select-none text-gray-400"
|
||||
>
|
||||
<span class="ml-[23.5px] whitespace-pre">{{
|
||||
@@ -142,7 +146,7 @@
|
||||
<TerminalSquareIcon class="ml-3 h-5 w-5" />
|
||||
</div>
|
||||
<input
|
||||
v-if="isServerRunning"
|
||||
v-if="isServerRunning && isConnected && !isWsAuthIncorrect"
|
||||
v-model="commandInput"
|
||||
type="text"
|
||||
placeholder="Send a command"
|
||||
@@ -168,29 +172,25 @@
|
||||
</UiServersPanelTerminal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersOverviewLoading v-else-if="!isConnected && !isWsAuthIncorrect" />
|
||||
<div v-else-if="isWsAuthIncorrect" class="flex flex-col">
|
||||
<h2>Could not connect to the server.</h2>
|
||||
<p>
|
||||
An error occurred while attempting to connect to your server. Please try refreshing the page.
|
||||
(WebSocket Authentication Failed)
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="flex flex-col">
|
||||
<h2>Could not connect to the server.</h2>
|
||||
<p>
|
||||
An error occurred while attempting to connect to your server. Please try refreshing the page.
|
||||
(No further information)
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="isWsAuthIncorrect"
|
||||
class="absolute inset-0 flex flex-col items-center justify-center bg-bg"
|
||||
>
|
||||
<h2>Could not connect to the server.</h2>
|
||||
<p>
|
||||
An error occurred while attempting to connect to your server. Please try refreshing the
|
||||
page. (WebSocket Authentication Failed)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { TerminalSquareIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import type { ServerState, Stats } from "~/types/servers";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { ServerState, Stats } from "@modrinth/utils";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
type ServerProps = {
|
||||
socket: WebSocket | null;
|
||||
@@ -203,7 +203,7 @@ type ServerProps = {
|
||||
exit_code?: number;
|
||||
};
|
||||
isServerRunning: boolean;
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
};
|
||||
|
||||
const props = defineProps<ServerProps>();
|
||||
|
||||
@@ -17,14 +17,14 @@ import {
|
||||
UserIcon,
|
||||
WrenchIcon,
|
||||
} 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";
|
||||
|
||||
const route = useRoute();
|
||||
const serverId = route.params.id as string;
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -114,11 +114,10 @@
|
||||
<script setup lang="ts">
|
||||
import { EditIcon, TransferIcon } from "@modrinth/assets";
|
||||
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
||||
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
|
||||
@@ -117,13 +117,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
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 serverId = route.params.id as string;
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
backupInProgress?: BackupInProgressReason;
|
||||
}>();
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<div class="relative h-full w-full overflow-y-auto">
|
||||
<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"
|
||||
>
|
||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
||||
@@ -73,7 +73,9 @@
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
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>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['network'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
@@ -273,10 +275,10 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
import { ButtonStyled, NewModal, ConfirmModal } from "@modrinth/ui";
|
||||
import { ref, computed, nextTick } from "vue";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const isUpdating = ref(false);
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStorage } from "@vueuse/core";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const route = useNativeRoute();
|
||||
const serverId = route.params.id as string;
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const preferences = {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<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 flex-col items-center text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
@@ -11,7 +14,9 @@
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
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>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['fs'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
@@ -141,10 +146,10 @@
|
||||
import { ref, watch, computed, inject } from "vue";
|
||||
import { EyeIcon, SearchIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import Fuse from "fuse.js";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const tags = useTags();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="relative h-full w-full">
|
||||
<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"
|
||||
>
|
||||
<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:
|
||||
</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>
|
||||
<ButtonStyled size="large" color="brand" @click="() => server.refresh(['startup'])">
|
||||
<button class="mt-6 !w-full">Retry</button>
|
||||
@@ -112,10 +114,10 @@
|
||||
<script setup lang="ts">
|
||||
import { UpdatedIcon, IssuesIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import type { Server } from "~/composables/pyroServers";
|
||||
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
||||
server: ModrinthServer;
|
||||
}>();
|
||||
|
||||
const data = computed(() => props.server.general);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<li v-if="fetchError" class="text-red">
|
||||
<p>Error details:</p>
|
||||
<UiCopyCode
|
||||
:text="(fetchError as PyroFetchError).message || 'Unknown error'"
|
||||
:text="(fetchError as ModrinthServersFetchError).message || 'Unknown error'"
|
||||
:copyable="false"
|
||||
:selectable="false"
|
||||
:language="'json'"
|
||||
@@ -121,9 +121,9 @@ import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import Fuse from "fuse.js";
|
||||
import { HammerIcon, PlusIcon, SearchIcon } from "@modrinth/assets";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import type { Server, ModrinthServersFetchError } from "@modrinth/utils";
|
||||
import { reloadNuxtApp } from "#app";
|
||||
import type { PyroFetchError } from "~/composables/pyroFetch";
|
||||
import type { Server } from "~/types/servers";
|
||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||
|
||||
definePageMeta({
|
||||
middleware: "auth",
|
||||
@@ -146,7 +146,9 @@ const {
|
||||
data: serverResponse,
|
||||
error: fetchError,
|
||||
refresh,
|
||||
} = await useAsyncData<ServerResponse>("ServerList", () => usePyroFetch<ServerResponse>("servers"));
|
||||
} = await useAsyncData<ServerResponse>("ServerList", () =>
|
||||
useServersFetch<ServerResponse>("servers"),
|
||||
);
|
||||
|
||||
watch([fetchError, serverResponse], ([error, response]) => {
|
||||
hasError.value = !!error || !response;
|
||||
|
||||
@@ -353,21 +353,6 @@
|
||||
Upgrade
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-if="
|
||||
getPyroCharge(subscription) &&
|
||||
getPyroCharge(subscription).status !== 'cancelled' &&
|
||||
getPyroCharge(subscription).status !== 'failed'
|
||||
"
|
||||
color="purple"
|
||||
color-fill="text"
|
||||
>
|
||||
<button @click="showPyroIntervalChange(subscription)">
|
||||
<TransferIcon />
|
||||
<!-- TODO: Make this attractive af for monthly subscribers -->
|
||||
Change billing interval
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
v-else-if="
|
||||
getPyroCharge(subscription) &&
|
||||
@@ -427,31 +412,6 @@
|
||||
:payment-methods="paymentMethods"
|
||||
:return-url="`${config.public.siteUrl}/settings/billing`"
|
||||
/>
|
||||
<PurchaseModal
|
||||
v-if="currentProduct"
|
||||
ref="pyroIntervalModal"
|
||||
:product="[currentProduct]"
|
||||
:country="country"
|
||||
custom-server
|
||||
interval-change-only
|
||||
:existing-subscription="currentSubscription"
|
||||
:existing-plan="currentProduct"
|
||||
:publishable-key="config.public.stripePublishableKey"
|
||||
:send-billing-request="
|
||||
async (body) => {
|
||||
await useBaseFetch(`billing/subscription/${currentSubscription.id}`, {
|
||||
internal: true,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
});
|
||||
}
|
||||
"
|
||||
:renewal-date="currentSubRenewalDate"
|
||||
:on-error="handleError"
|
||||
:customer="customer"
|
||||
:payment-methods="paymentMethods"
|
||||
:return-url="`${config.public.siteUrl}/settings/billing`"
|
||||
/>
|
||||
<PurchaseModal
|
||||
ref="pyroPurchaseModal"
|
||||
:product="upgradeProducts"
|
||||
@@ -632,6 +592,7 @@ import {
|
||||
} from "@modrinth/assets";
|
||||
import { calculateSavings, formatPrice, getCurrency } from "@modrinth/utils";
|
||||
import { ref, computed } from "vue";
|
||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||
import { products } from "~/generated/state.json";
|
||||
|
||||
definePageMeta({
|
||||
@@ -784,7 +745,7 @@ const [
|
||||
useBaseFetch("billing/subscriptions", { 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"));
|
||||
@@ -862,18 +823,6 @@ const oppositeInterval = computed(() =>
|
||||
midasCharge.value?.subscription_interval === "yearly" ? "monthly" : "yearly",
|
||||
);
|
||||
|
||||
async function showPyroIntervalChange(subscription) {
|
||||
currentSubscription.value = subscription;
|
||||
currentSubRenewalDate.value = getPyroCharge(subscription).due;
|
||||
currentProduct.value = getPyroProduct(subscription);
|
||||
|
||||
upgradeProducts.value = [currentProduct.value];
|
||||
upgradeProducts.value.metadata = { type: "pyro" };
|
||||
|
||||
await nextTick();
|
||||
pyroIntervalModal.value.show();
|
||||
}
|
||||
|
||||
async function switchMidasInterval(interval) {
|
||||
changingInterval.value = true;
|
||||
startLoading();
|
||||
@@ -993,7 +942,6 @@ const getProductPrice = (product, interval) => {
|
||||
const modalCancel = ref(null);
|
||||
|
||||
const pyroPurchaseModal = ref();
|
||||
const pyroIntervalModal = ref();
|
||||
const currentSubscription = ref(null);
|
||||
const currentProduct = ref(null);
|
||||
const upgradeProducts = ref([]);
|
||||
@@ -1037,7 +985,7 @@ async function fetchCapacityStatuses(serverId, product) {
|
||||
if (product) {
|
||||
try {
|
||||
return {
|
||||
custom: await usePyroFetch(`servers/${serverId}/upgrade-stock`, {
|
||||
custom: await useServersFetch(`servers/${serverId}/upgrade-stock`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
cpu: product.metadata.cpu,
|
||||
|
||||
@@ -303,7 +303,7 @@
|
||||
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<nuxt-link
|
||||
v-for="org in organizations"
|
||||
v-for="org in sortedOrgs"
|
||||
:key="org.id"
|
||||
v-tooltip="org.name"
|
||||
class="organization"
|
||||
@@ -516,6 +516,8 @@ try {
|
||||
});
|
||||
}
|
||||
|
||||
const sortedOrgs = computed(() => organizations.value.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
if (!user.value) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
|
||||
@@ -18,7 +18,7 @@ const initialBatchSize = 256;
|
||||
* @property {function(string): void} addConsoleOutput - Method to add a new console output line
|
||||
* @property {function(): void} clear - Method to clear all console output
|
||||
*/
|
||||
export const usePyroConsole = createGlobalState(() => {
|
||||
export const useModrinthServersConsole = createGlobalState(() => {
|
||||
/**
|
||||
* Reactive array storing console output lines
|
||||
* @type {Ref<string[]>}
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
// export interface Mod {
|
||||
// id: string;
|
||||
// filename: string;
|
||||
// modrinth_ids: {
|
||||
// project_id: string;
|
||||
// version_id: string;
|
||||
// };
|
||||
// }
|
||||
|
||||
interface License {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface DonationUrl {
|
||||
id: string;
|
||||
platform: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface GalleryItem {
|
||||
url: string;
|
||||
featured: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
created: string;
|
||||
ordering: number;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
categories: string[];
|
||||
client_side: "required" | "optional";
|
||||
server_side: "required" | "optional";
|
||||
body: string;
|
||||
status: "approved" | "pending" | "rejected";
|
||||
requested_status: "approved" | "pending" | "rejected";
|
||||
additional_categories: string[];
|
||||
issues_url: string;
|
||||
source_url: string;
|
||||
wiki_url: string;
|
||||
discord_url: string;
|
||||
donation_urls: DonationUrl[];
|
||||
project_type: "mod" | "resourcepack" | "map" | "plugin";
|
||||
downloads: number;
|
||||
icon_url: string;
|
||||
color: number;
|
||||
thread_id: string;
|
||||
monetization_status: "monetized" | "non-monetized";
|
||||
id: string;
|
||||
team: string;
|
||||
body_url: string | null;
|
||||
moderator_message: string | null;
|
||||
published: string;
|
||||
updated: string;
|
||||
approved: string;
|
||||
queued: string;
|
||||
followers: number;
|
||||
license: License;
|
||||
versions: string[];
|
||||
game_versions: string[];
|
||||
loaders: string[];
|
||||
gallery: GalleryItem[];
|
||||
}
|
||||
|
||||
export interface ServerBackup {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Allocation {
|
||||
name: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
server_id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
net: {
|
||||
ip: string;
|
||||
port: number;
|
||||
domain: string;
|
||||
allocations: Allocation[];
|
||||
};
|
||||
game: string;
|
||||
loader: string | null;
|
||||
loader_version: string | null;
|
||||
mc_version: string | null;
|
||||
backup_quota: number;
|
||||
used_backup_quota: number;
|
||||
backups: ServerBackup[];
|
||||
mods: Mod[];
|
||||
project: Project | null;
|
||||
suspension_reason: string | null;
|
||||
image: string | null;
|
||||
upstream?: {
|
||||
kind: "modpack";
|
||||
project_id: string;
|
||||
version_id: string;
|
||||
};
|
||||
motd: string;
|
||||
flows: {
|
||||
intro?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
current: {
|
||||
cpu_percent: number;
|
||||
ram_usage_bytes: number;
|
||||
ram_total_bytes: number;
|
||||
storage_usage_bytes: number;
|
||||
storage_total_bytes: number;
|
||||
};
|
||||
past: {
|
||||
cpu_percent: number;
|
||||
ram_usage_bytes: number;
|
||||
ram_total_bytes: number;
|
||||
storage_usage_bytes: number;
|
||||
storage_total_bytes: number;
|
||||
};
|
||||
graph: {
|
||||
cpu: number[];
|
||||
ram: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WSAuth {
|
||||
url: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export type ServerState = "running" | "stopped" | "crashed";
|
||||
// export type WebsocketEventType =
|
||||
// | "log"
|
||||
// | "auth"
|
||||
// | "stats"
|
||||
// | "power-state"
|
||||
// | "auth-expiring"
|
||||
// | "auth-incorrect"
|
||||
// | "installation-result"
|
||||
// | (string & {});
|
||||
|
||||
// export interface WSEvent {
|
||||
// event: WebsocketEventType;
|
||||
// message: string;
|
||||
// state: ServerState;
|
||||
// }
|
||||
|
||||
export type Loaders =
|
||||
| "Fabric"
|
||||
| "Quilt"
|
||||
| "Forge"
|
||||
| "NeoForge"
|
||||
| "Paper"
|
||||
| "Spigot"
|
||||
| "Bukkit"
|
||||
| "Vanilla"
|
||||
| "Purpur";
|
||||
|
||||
export interface WSLogEvent {
|
||||
event: "log";
|
||||
message: string;
|
||||
}
|
||||
|
||||
type CurrentStats = Stats["current"];
|
||||
|
||||
export interface WSStatsEvent extends CurrentStats {
|
||||
event: "stats";
|
||||
}
|
||||
|
||||
export interface WSAuthExpiringEvent {
|
||||
event: "auth-expiring";
|
||||
}
|
||||
|
||||
export interface WSPowerStateEvent {
|
||||
event: "power-state";
|
||||
state: ServerState;
|
||||
// if state "crashed"
|
||||
oom_killed?: boolean;
|
||||
exit_code?: number;
|
||||
}
|
||||
|
||||
export interface WSAuthIncorrectEvent {
|
||||
event: "auth-incorrect";
|
||||
}
|
||||
|
||||
export interface WSInstallationResultOkEvent {
|
||||
event: "installation-result";
|
||||
result: "ok";
|
||||
}
|
||||
|
||||
export interface WSInstallationResultErrEvent {
|
||||
event: "installation-result";
|
||||
result: "err";
|
||||
reason: string;
|
||||
}
|
||||
export type WSInstallationResultEvent = WSInstallationResultOkEvent | WSInstallationResultErrEvent;
|
||||
|
||||
export interface WSAuthOkEvent {
|
||||
event: "auth-ok";
|
||||
}
|
||||
|
||||
export interface WSUptimeEvent {
|
||||
event: "uptime";
|
||||
uptime: number; // seconds
|
||||
}
|
||||
|
||||
export interface WSNewModEvent {
|
||||
event: "new-mod";
|
||||
}
|
||||
|
||||
export type WSBackupTask = "file" | "create" | "restore";
|
||||
export type WSBackupState = "ongoing" | "done" | "failed" | "cancelled" | "unchanged";
|
||||
|
||||
export interface WSBackupProgressEvent {
|
||||
event: "backup-progress";
|
||||
task: WSBackupTask;
|
||||
id: string;
|
||||
progress: number; // percentage
|
||||
state: WSBackupState;
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export type FSQueuedOpUnarchive = {
|
||||
op: "unarchive";
|
||||
src: string;
|
||||
};
|
||||
|
||||
export type FSQueuedOp = FSQueuedOpUnarchive;
|
||||
|
||||
export type FSOpUnarchive = {
|
||||
op: "unarchive";
|
||||
progress: number; // Note: 1 does not mean it's done
|
||||
id: string; // UUID
|
||||
|
||||
mime: string;
|
||||
src: string;
|
||||
state:
|
||||
| "queued"
|
||||
| "ongoing"
|
||||
| "cancelled"
|
||||
| "done"
|
||||
| "failed-corrupted"
|
||||
| "failed-invalid-path"
|
||||
| "failed-cf-no-serverpack"
|
||||
| "failed-cf-not-available"
|
||||
| "failed-not-reachable";
|
||||
|
||||
current_file: string | null;
|
||||
failed_path?: string;
|
||||
bytes_processed: number;
|
||||
files_processed: number;
|
||||
started: string;
|
||||
};
|
||||
|
||||
export type FilesystemOp = FSOpUnarchive;
|
||||
|
||||
export interface WSFilesystemOpsEvent {
|
||||
event: "filesystem-ops";
|
||||
all: FilesystemOp[];
|
||||
}
|
||||
|
||||
export type WSEvent =
|
||||
| WSLogEvent
|
||||
| WSStatsEvent
|
||||
| WSPowerStateEvent
|
||||
| WSAuthExpiringEvent
|
||||
| WSAuthIncorrectEvent
|
||||
| WSInstallationResultEvent
|
||||
| WSAuthOkEvent
|
||||
| WSUptimeEvent
|
||||
| WSNewModEvent
|
||||
| WSBackupProgressEvent
|
||||
| WSFilesystemOpsEvent;
|
||||
|
||||
export interface Servers {
|
||||
servers: Server[];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user