You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit 'dbde3c4669af10dd577590ed6980e5bd4552d13c' into feature-clean
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Windows has stack overflows when calling from Tauri, so we increase compiler size
|
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
|
|||||||
14
.github/workflows/theseus-release.yml
vendored
14
.github/workflows/theseus-release.yml
vendored
@@ -33,16 +33,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Rust setup (mac)
|
- name: Rust setup (mac)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
|
|
||||||
- name: Rust setup
|
- name: Rust setup
|
||||||
if: "!startsWith(matrix.platform, 'macos')"
|
if: "!startsWith(matrix.platform, 'macos')"
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
|
|
||||||
- name: Setup rust cache
|
- name: Setup rust cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -73,10 +73,10 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-rust-target-
|
${{ runner.os }}-rust-target-
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: .nvmrc
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: Install pnpm via corepack
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="CommitMessageInspectionProfile">
|
||||||
|
<profile version="1.0">
|
||||||
|
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
787
Cargo.lock
generated
787
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
93
Cargo.toml
93
Cargo.toml
@@ -10,6 +10,9 @@ members = [
|
|||||||
"packages/daedalus",
|
"packages/daedalus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-cors = "0.7.1"
|
actix-cors = "0.7.1"
|
||||||
actix-files = "0.6.6"
|
actix-files = "0.6.6"
|
||||||
@@ -21,7 +24,8 @@ actix-web-prom = "0.10.0"
|
|||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
async-compression = { version = "0.4.23", default-features = false }
|
async_zip = "0.0.17"
|
||||||
|
async-compression = { version = "0.4.24", default-features = false }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
"runtime-tokio-hyper-rustls",
|
"runtime-tokio-hyper-rustls",
|
||||||
@@ -31,14 +35,14 @@ async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
|||||||
"futures-03-sink",
|
"futures-03-sink",
|
||||||
] }
|
] }
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
async_zip = "0.0.17"
|
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.0"
|
bitflags = "2.9.1"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
censor = "0.3.0"
|
censor = "0.3.0"
|
||||||
|
chardetng = "0.1.17"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
clap = "4.5.38"
|
clap = "4.5.40"
|
||||||
clickhouse = "0.13.2"
|
clickhouse = "0.13.3"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
console-subscriber = "0.4.1"
|
console-subscriber = "0.4.1"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
@@ -50,8 +54,9 @@ dotenv-build = "0.1.1"
|
|||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
|
encoding_rs = "0.8.35"
|
||||||
enumset = "1.1.6"
|
enumset = "1.1.6"
|
||||||
flate2 = "1.1.1"
|
flate2 = "1.1.2"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = { version = "0.3.31", default-features = false }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
@@ -59,7 +64,7 @@ hex = "0.4.3"
|
|||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper-tls = "0.6.0"
|
hyper-tls = "0.6.0"
|
||||||
hyper-util = "0.1.11"
|
hyper-util = "0.1.14"
|
||||||
iana-time-zone = "0.1.63"
|
iana-time-zone = "0.1.63"
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||||
indexmap = "2.9.0"
|
indexmap = "2.9.0"
|
||||||
@@ -67,7 +72,7 @@ indicatif = "0.17.11"
|
|||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jemalloc_pprof = "0.7.0"
|
jemalloc_pprof = "0.7.0"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.0.0", default-features = false }
|
||||||
lettre = { version = "0.11.16", default-features = false, features = [
|
lettre = { version = "0.11.17", default-features = false, features = [
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
@@ -89,19 +94,19 @@ quartz_nbt = "0.2.9"
|
|||||||
quick-xml = "0.37.5"
|
quick-xml = "0.37.5"
|
||||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||||
redis = "0.31.0"
|
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.15", default-features = false }
|
reqwest = { version = "0.12.19", default-features = false }
|
||||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
|
||||||
"fail-on-err",
|
|
||||||
"tags",
|
|
||||||
"tokio-rustls-tls",
|
|
||||||
] }
|
|
||||||
rust_decimal = { version = "1.37.1", features = [
|
rust_decimal = { version = "1.37.1", features = [
|
||||||
"serde-with-float",
|
"serde-with-float",
|
||||||
"serde-with-str",
|
"serde-with-str",
|
||||||
] }
|
] }
|
||||||
rust_iso3166 = "0.1.14"
|
rust_iso3166 = "0.1.14"
|
||||||
|
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||||
|
"fail-on-err",
|
||||||
|
"tags",
|
||||||
|
"tokio-rustls-tls",
|
||||||
|
] }
|
||||||
rusty-money = "0.4.1"
|
rusty-money = "0.4.1"
|
||||||
sentry = { version = "0.38.1", default-features = false, features = [
|
sentry = { version = "0.38.1", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
@@ -113,26 +118,26 @@ sentry = { version = "0.38.1", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
sentry-actix = "0.38.1"
|
sentry-actix = "0.38.1"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
serde-xml-rs = "0.8.0" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
|
||||||
serde_bytes = "0.11.17"
|
serde_bytes = "0.11.17"
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
serde_ini = "0.2.0"
|
serde_ini = "0.2.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
serde_with = "3.12.0"
|
serde_with = "3.12.0"
|
||||||
|
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
spdx = "0.10.8"
|
spdx = "0.10.8"
|
||||||
sqlx = { version = "0.8.5", default-features = false }
|
sqlx = { version = "0.8.6", default-features = false }
|
||||||
sysinfo = { version = "0.35.1", default-features = false }
|
sysinfo = { version = "0.35.2", default-features = false }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tauri = "2.5.1"
|
tauri = "2.5.1"
|
||||||
tauri-build = "2.2.0"
|
tauri-build = "2.2.0"
|
||||||
tauri-plugin-deep-link = "2.2.1"
|
tauri-plugin-deep-link = "2.3.0"
|
||||||
tauri-plugin-dialog = "2.2.1"
|
tauri-plugin-dialog = "2.2.2"
|
||||||
tauri-plugin-opener = "2.2.6"
|
tauri-plugin-opener = "2.2.7"
|
||||||
tauri-plugin-os = "2.2.1"
|
tauri-plugin-os = "2.2.1"
|
||||||
tauri-plugin-single-instance = "2.2.3"
|
tauri-plugin-single-instance = "2.2.4"
|
||||||
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"zip",
|
"zip",
|
||||||
@@ -143,7 +148,7 @@ theseus = { path = "packages/app-lib" }
|
|||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tikv-jemalloc-ctl = "0.6.0"
|
tikv-jemalloc-ctl = "0.6.0"
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
tokio = "1.45.0"
|
tokio = "1.45.1"
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.15"
|
||||||
totp-rs = "5.7.0"
|
totp-rs = "5.7.0"
|
||||||
@@ -153,14 +158,14 @@ tracing-error = "0.2.1"
|
|||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
uuid = "1.16.0"
|
uuid = "1.17.0"
|
||||||
validator = "0.20.0"
|
validator = "0.20.0"
|
||||||
webp = { version = "0.3.0", default-features = false }
|
webp = { version = "0.3.0", default-features = false }
|
||||||
whoami = "1.6.0"
|
whoami = "1.6.0"
|
||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
woothee = "0.13.0"
|
woothee = "0.13.0"
|
||||||
yaserde = "0.12.0"
|
yaserde = "0.12.0"
|
||||||
zip = { version = "3.0.0", default-features = false, features = [
|
zip = { version = "4.0.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -168,6 +173,44 @@ zip = { version = "3.0.0", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
zxcvbn = "3.1.0"
|
zxcvbn = "3.1.0"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
bool_to_int_with_if = "warn"
|
||||||
|
borrow_as_ptr = "warn"
|
||||||
|
cfg_not_test = "warn"
|
||||||
|
clear_with_drain = "warn"
|
||||||
|
cloned_instead_of_copied = "warn"
|
||||||
|
collection_is_never_read = "warn"
|
||||||
|
dbg_macro = "warn"
|
||||||
|
default_trait_access = "warn"
|
||||||
|
explicit_iter_loop = "warn"
|
||||||
|
filter_map_next = "warn"
|
||||||
|
flat_map_option = "warn"
|
||||||
|
format_push_string = "warn"
|
||||||
|
get_unwrap = "warn"
|
||||||
|
large_include_file = "warn"
|
||||||
|
large_stack_arrays = "warn"
|
||||||
|
manual_assert = "warn"
|
||||||
|
manual_instant_elapsed = "warn"
|
||||||
|
manual_is_variant_and = "warn"
|
||||||
|
manual_let_else = "warn"
|
||||||
|
map_unwrap_or = "warn"
|
||||||
|
match_bool = "warn"
|
||||||
|
needless_collect = "warn"
|
||||||
|
negative_feature_names = "warn"
|
||||||
|
non_std_lazy_statics = "warn"
|
||||||
|
pathbuf_init_then_push = "warn"
|
||||||
|
read_zero_byte_vec = "warn"
|
||||||
|
redundant_clone = "warn"
|
||||||
|
redundant_feature_names = "warn"
|
||||||
|
redundant_type_annotations = "warn"
|
||||||
|
todo = "warn"
|
||||||
|
unnested_or_patterns = "warn"
|
||||||
|
wildcard_dependencies = "warn"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
# Turn warnings into errors by default
|
||||||
|
warnings = "deny"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
||||||
|
|
||||||
|
|||||||
@@ -9,18 +9,19 @@
|
|||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
|
"test": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
"@modrinth/ui": "workspace:*",
|
"@modrinth/ui": "workspace:*",
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
"@sentry/vue": "^8.27.0",
|
"@sentry/vue": "^8.27.0",
|
||||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-os": "^2.2.1",
|
|
||||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||||
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||||
"@vintl/vintl": "^4.4.1",
|
"@vintl/vintl": "^4.4.1",
|
||||||
@@ -39,11 +40,12 @@
|
|||||||
"@eslint/compat": "^1.1.1",
|
"@eslint/compat": "^1.1.1",
|
||||||
"@formatjs/cli": "^6.2.12",
|
"@formatjs/cli": "^6.2.12",
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
|
"@taijased/vue-render-tracker": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-config-custom": "workspace:*",
|
"eslint-config-custom": "workspace:*",
|
||||||
"eslint-plugin-turbo": "^2.1.1",
|
"eslint-plugin-turbo": "^2.5.4",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.74.1",
|
"sass": "^1.74.1",
|
||||||
@@ -51,8 +53,7 @@
|
|||||||
"tsconfig": "workspace:*",
|
"tsconfig": "workspace:*",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.6",
|
||||||
"vue-tsc": "^2.1.6",
|
"vue-tsc": "^2.1.6"
|
||||||
"@taijased/vue-render-tracker": "^1.0.7"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.4.0",
|
"packageManager": "pnpm@9.4.0",
|
||||||
"web-types": "../../web-types.json"
|
"web-types": "../../web-types.json"
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ const editProfileObject = computed(() => {
|
|||||||
hooks?: Hooks
|
hooks?: Hooks
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (overrideHooks.value) {
|
// When hooks are not overridden per-instance, we want to clear them
|
||||||
editProfile.hooks = hooks.value
|
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
||||||
import {
|
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||||
set_world_display_status,
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
getWorldIdentifier,
|
|
||||||
showWorldInFolder,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { formatNumber } from '@modrinth/utils'
|
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -49,6 +45,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||||
|
(e: 'open-folder', world: SingleplayerWorld): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -108,20 +105,6 @@ const serverIncompatible = computed(
|
|||||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||||
)
|
)
|
||||||
|
|
||||||
function getPingLevel(ping: number) {
|
|
||||||
if (ping < 150) {
|
|
||||||
return 5
|
|
||||||
} else if (ping < 300) {
|
|
||||||
return 4
|
|
||||||
} else if (ping < 600) {
|
|
||||||
return 3
|
|
||||||
} else if (ping < 1000) {
|
|
||||||
return 2
|
|
||||||
} else {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -394,8 +377,7 @@ const messages = defineMessages({
|
|||||||
{
|
{
|
||||||
id: 'open-folder',
|
id: 'open-folder',
|
||||||
shown: world.type === 'singleplayer',
|
shown: world.type === 'singleplayer',
|
||||||
action: () =>
|
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
divider: true,
|
divider: true,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ function show(world: SingleplayerWorld) {
|
|||||||
icon.value = world.icon
|
icon.value = world.icon
|
||||||
displayStatus.value = world.display_status
|
displayStatus.value = world.display_status
|
||||||
hideFromHome.value = world.display_status === 'hidden'
|
hideFromHome.value = world.display_status === 'hidden'
|
||||||
|
removeIcon.value = false
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,11 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-if="instance.install_stage.includes('installing')"
|
v-if="
|
||||||
|
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
||||||
|
instance.install_stage,
|
||||||
|
)
|
||||||
|
"
|
||||||
color="brand"
|
color="brand"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
||||||
"
|
"
|
||||||
@delete="() => promptToRemoveWorld(world)"
|
@delete="() => promptToRemoveWorld(world)"
|
||||||
|
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,6 +152,7 @@ import {
|
|||||||
hasQuickPlaySupport,
|
hasQuickPlaySupport,
|
||||||
refreshWorlds,
|
refreshWorlds,
|
||||||
handleDefaultProfileUpdateEvent,
|
handleDefaultProfileUpdateEvent,
|
||||||
|
showWorldInFolder,
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_playground"
|
name = "theseus_playground"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
edition = "2024"
|
edition.workspace = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -9,3 +9,6 @@ edition = "2024"
|
|||||||
theseus = { workspace = true, features = ["cli"] }
|
theseus = { workspace = true, features = ["cli"] }
|
||||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
enumset.workspace = true
|
enumset.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
"name": "@modrinth/app-playground",
|
"name": "@modrinth/app-playground",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix",
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo test"
|
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ version = "0.9.5"
|
|||||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/modrinth/code/apps/app/"
|
repository = "https://github.com/modrinth/code/apps/app/"
|
||||||
edition = "2024"
|
edition.workspace = true
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { workspace = true, features = ["codegen"] }
|
tauri-build = { workspace = true, features = ["codegen"] }
|
||||||
@@ -17,7 +16,7 @@ serde_json.workspace = true
|
|||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
|
|
||||||
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset"] }
|
||||||
tauri-plugin-window-state.workspace = true
|
tauri-plugin-window-state.workspace = true
|
||||||
tauri-plugin-deep-link.workspace = true
|
tauri-plugin-deep-link.workspace = true
|
||||||
tauri-plugin-os.workspace = true
|
tauri-plugin-os.workspace = true
|
||||||
@@ -56,3 +55,6 @@ default = ["custom-protocol"]
|
|||||||
# DO NOT remove this
|
# DO NOT remove this
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
updater = []
|
updater = []
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app",
|
"name": "@modrinth/app",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tauri build",
|
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
|
"build": "tauri build",
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"test": "cargo test",
|
"test": "cargo nextest run --all-targets --no-fail-fast",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix"
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "2.5.0"
|
"@tauri-apps/cli": "2.5.0"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ pub fn get_os() -> OS {
|
|||||||
let os = OS::MacOS;
|
let os = OS::MacOS;
|
||||||
os
|
os
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum OS {
|
pub enum OS {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub async fn get_recent_worlds<R: Runtime>(
|
|||||||
display_statuses.unwrap_or(EnumSet::all()),
|
display_statuses.unwrap_or(EnumSet::all()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for world in result.iter_mut() {
|
for world in &mut result {
|
||||||
adapt_world_icon(&app_handle, &mut world.world);
|
adapt_world_icon(&app_handle, &mut world.world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -55,7 +55,7 @@ pub async fn get_profile_worlds<R: Runtime>(
|
|||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<Vec<World>> {
|
) -> Result<Vec<World>> {
|
||||||
let mut result = worlds::get_profile_worlds(path).await?;
|
let mut result = worlds::get_profile_worlds(path).await?;
|
||||||
for world in result.iter_mut() {
|
for world in &mut result {
|
||||||
adapt_world_icon(&app_handle, world);
|
adapt_world_icon(&app_handle, world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager: &M,
|
manager: &M,
|
||||||
) -> InitialPayload {
|
) -> InitialPayload {
|
||||||
let initial_payload = manager.try_state::<InitialPayload>();
|
let initial_payload = manager.try_state::<InitialPayload>();
|
||||||
let mtx = if let Some(initial_payload) = initial_payload {
|
|
||||||
|
if let Some(initial_payload) = initial_payload {
|
||||||
initial_payload.inner().clone()
|
initial_payload.inner().clone()
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("No initial payload found, creating new");
|
tracing::info!("No initial payload found, creating new");
|
||||||
@@ -22,7 +23,5 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager.manage(payload.clone());
|
manager.manage(payload.clone());
|
||||||
|
|
||||||
payload
|
payload
|
||||||
};
|
}
|
||||||
|
|
||||||
mtx
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ fn main() {
|
|||||||
{
|
{
|
||||||
let payload = macos::deep_link::get_or_init_payload(app);
|
let payload = macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload.clone();
|
let mtx_copy = payload.payload;
|
||||||
app.listen("deep-link://new-url", move |url| {
|
app.listen("deep-link://new-url", move |url| {
|
||||||
let mtx_copy_copy = mtx_copy.clone();
|
let mtx_copy_copy = mtx_copy.clone();
|
||||||
let request = url.payload().to_owned();
|
let request = url.payload().to_owned();
|
||||||
@@ -242,7 +242,6 @@ fn main() {
|
|||||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||||
payload,
|
payload,
|
||||||
));
|
));
|
||||||
dbg!(url);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
@@ -285,22 +284,22 @@ fn main() {
|
|||||||
|
|
||||||
match app {
|
match app {
|
||||||
Ok(app) => {
|
Ok(app) => {
|
||||||
#[allow(unused_variables)]
|
|
||||||
app.run(|app, event| {
|
app.run(|app, event| {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
drop((app, event));
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let tauri::RunEvent::Opened { urls } = event {
|
if let tauri::RunEvent::Opened { urls } = event {
|
||||||
tracing::info!("Handling webview open {urls:?}");
|
tracing::info!("Handling webview open {urls:?}");
|
||||||
|
|
||||||
let file = urls
|
let file = urls
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|url| url.to_file_path().ok())
|
.find_map(|url| url.to_file_path().ok());
|
||||||
.next();
|
|
||||||
|
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
let payload =
|
let payload =
|
||||||
macos::deep_link::get_or_init_payload(app);
|
macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload.clone();
|
let mtx_copy = payload.payload;
|
||||||
let request = file.to_string_lossy().to_string();
|
let request = file.to_string_lossy().to_string();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut payload = mtx_copy.lock().await;
|
let mut payload = mtx_copy.lock().await;
|
||||||
|
|||||||
14
apps/app/turbo.jsonc
Normal file
14
apps/app/turbo.jsonc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/turbo/schema.json",
|
||||||
|
"extends": ["//"],
|
||||||
|
"tasks": {
|
||||||
|
// Running Clippy and tests on a Tauri application requires
|
||||||
|
// the frontend to be built at least once first
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "daedalus_client"
|
name = "daedalus_client"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
authors = ["Jai A <jai@modrinth.com>"]
|
authors = ["Jai A <jai@modrinth.com>"]
|
||||||
edition = "2024"
|
edition.workspace = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -28,3 +28,6 @@ tracing-error.workspace = true
|
|||||||
|
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM rust:1.86.0 AS build
|
FROM rust:1.87.0 AS build
|
||||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
|
||||||
WORKDIR /usr/src/daedalus
|
WORKDIR /usr/src/daedalus
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
"name": "@modrinth/daedalus_client",
|
"name": "@modrinth/daedalus_client",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix",
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo test"
|
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/daedalus": "workspace:*"
|
"@modrinth/daedalus": "workspace:*"
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ pub async fn fetch(
|
|||||||
if modrinth_version
|
if modrinth_version
|
||||||
.original_sha1
|
.original_sha1
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|x| x == &version.sha1)
|
.is_some_and(|x| x == &version.sha1)
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
{
|
||||||
existing_versions.push(modrinth_version);
|
existing_versions.push(modrinth_version);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro check && astro build",
|
"lint": "astro check",
|
||||||
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
@@ -18,4 +19,4 @@
|
|||||||
"starlight-openapi": "^0.14.0",
|
"starlight-openapi": "^0.14.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ This project is part of our [monorepo](https://github.com/modrinth/code). You ca
|
|||||||
|
|
||||||
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432 and a MeiliSearch instance on port 7700. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
|
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432 and a MeiliSearch instance on port 7700. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
|
||||||
|
|
||||||
To get a basic configuration, copy the `.env.example` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:
|
To get a basic configuration, copy the `.env.local` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install --git https://github.com/launchbadge/sqlx sqlx-cli --no-default-features --features postgres,rustls
|
cargo install --git https://github.com/launchbadge/sqlx sqlx-cli --no-default-features --features postgres,rustls
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
BASE_URL=https://api.modrinth.com/v2/
|
|
||||||
BROWSER_BASE_URL=https://api.modrinth.com/v2/
|
|
||||||
PYRO_BASE_URL=https://archon.modrinth.com/
|
|
||||||
4
apps/frontend/.env.local
Normal file
4
apps/frontend/.env.local
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
BASE_URL=http://127.0.0.1:8000/v2/
|
||||||
|
BROWSER_BASE_URL=http://127.0.0.1:8000/v2/
|
||||||
|
PYRO_BASE_URL=https://staging-archon.modrinth.com
|
||||||
|
PROD_OVERRIDE=true
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"postinstall": "nuxi prepare",
|
"postinstall": "nuxi prepare",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
|
"test": "nuxi build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formatjs/cli": "^6.2.12",
|
"@formatjs/cli": "^6.2.12",
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<OmorphiaAvatar
|
|
||||||
:src="src"
|
|
||||||
:alt="alt"
|
|
||||||
:size="size"
|
|
||||||
:circle="circle"
|
|
||||||
:no-shadow="noShadow"
|
|
||||||
:loading="loading"
|
|
||||||
:raised="raised"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { Avatar as OmorphiaAvatar } from "@modrinth/ui";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
alt: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: "2rem",
|
|
||||||
},
|
|
||||||
circle: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
noShadow: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: String,
|
|
||||||
default: "eager",
|
|
||||||
},
|
|
||||||
raised: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span
|
|
||||||
:class="
|
|
||||||
'badge flex items-center gap-1 font-semibold text-secondary ' + color + ' type--' + type
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<template v-if="color"> <span class="circle" /> {{ capitalizeString(type) }}</template>
|
|
||||||
|
|
||||||
<!-- User roles -->
|
|
||||||
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team</template>
|
|
||||||
<template v-else-if="type === 'moderator'"> <ModeratorIcon /> Moderator</template>
|
|
||||||
<template v-else-if="type === 'creator'"><CreatorIcon /> Creator</template>
|
|
||||||
<template v-else-if="type === 'plus'"><PlusIcon /> Modrinth Plus</template>
|
|
||||||
|
|
||||||
<!-- Project statuses -->
|
|
||||||
<template v-else-if="type === 'approved'"><GlobeIcon /> Public</template>
|
|
||||||
<template v-else-if="type === 'approved-general'"><CheckIcon /> Approved</template>
|
|
||||||
<template v-else-if="type === 'unlisted' || type === 'withheld'"
|
|
||||||
><LinkIcon /> Unlisted</template
|
|
||||||
>
|
|
||||||
<template v-else-if="type === 'private'"><LockIcon /> Private</template>
|
|
||||||
<template v-else-if="type === 'scheduled'"> <CalendarIcon /> Scheduled</template>
|
|
||||||
<template v-else-if="type === 'draft'"><DraftIcon /> Draft</template>
|
|
||||||
<template v-else-if="type === 'archived'"> <ArchiveIcon /> Archived</template>
|
|
||||||
<template v-else-if="type === 'rejected'"><CrossIcon /> Rejected</template>
|
|
||||||
<template v-else-if="type === 'processing'"> <ProcessingIcon /> Under review</template>
|
|
||||||
|
|
||||||
<!-- Team members -->
|
|
||||||
<template v-else-if="type === 'accepted'"><CheckIcon /> Accepted</template>
|
|
||||||
<template v-else-if="type === 'pending'"> <ProcessingIcon /> Pending </template>
|
|
||||||
|
|
||||||
<!-- Transaction statuses -->
|
|
||||||
<template v-else-if="type === 'success'"><CheckIcon /> Success</template>
|
|
||||||
|
|
||||||
<!-- Report status -->
|
|
||||||
<template v-else-if="type === 'closed'"> <CloseIcon /> Closed</template>
|
|
||||||
|
|
||||||
<!-- Other -->
|
|
||||||
<template v-else> <span class="circle" /> {{ capitalizeString(type) }} </template>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
GlobeIcon,
|
|
||||||
LinkIcon,
|
|
||||||
ModrinthIcon,
|
|
||||||
PlusIcon,
|
|
||||||
ScaleIcon as ModeratorIcon,
|
|
||||||
BoxIcon as CreatorIcon,
|
|
||||||
FileTextIcon as DraftIcon,
|
|
||||||
XIcon as CrossIcon,
|
|
||||||
ArchiveIcon,
|
|
||||||
UpdatedIcon as ProcessingIcon,
|
|
||||||
CheckIcon,
|
|
||||||
LockIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
XCircleIcon as CloseIcon,
|
|
||||||
} from "@modrinth/assets";
|
|
||||||
import { capitalizeString } from "@modrinth/utils";
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.badge {
|
|
||||||
.circle {
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
background-color: var(--badge-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
vertical-align: -15%;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.type--closed,
|
|
||||||
&.type--withheld,
|
|
||||||
&.type--rejected,
|
|
||||||
&.red {
|
|
||||||
--badge-color: var(--color-red);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.type--pending,
|
|
||||||
&.type--moderator,
|
|
||||||
&.type--processing,
|
|
||||||
&.type--scheduled,
|
|
||||||
&.orange {
|
|
||||||
--badge-color: var(--color-orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.type--accepted,
|
|
||||||
&.type--admin,
|
|
||||||
&.type--success,
|
|
||||||
&.type--approved-general,
|
|
||||||
&.green {
|
|
||||||
--badge-color: var(--color-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.type--creator,
|
|
||||||
&.blue {
|
|
||||||
--badge-color: var(--color-blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.type--unlisted,
|
|
||||||
&.type--plus,
|
|
||||||
&.purple {
|
|
||||||
--badge-color: var(--color-purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.type--private,
|
|
||||||
&.type--approved,
|
|
||||||
&.gray {
|
|
||||||
--badge-color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button class="code" :class="{ copied }" title="Copy code to clipboard" @click="copyText">
|
|
||||||
<span>{{ text }}</span>
|
|
||||||
<CheckIcon v-if="copied" />
|
|
||||||
<ClipboardCopyIcon v-else />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { CheckIcon, ClipboardCopyIcon } from "@modrinth/assets";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
CheckIcon,
|
|
||||||
ClipboardCopyIcon,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
text: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
copied: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async copyText() {
|
|
||||||
await navigator.clipboard.writeText(this.text);
|
|
||||||
this.copied = true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.code {
|
|
||||||
color: var(--color-text);
|
|
||||||
display: inline-flex;
|
|
||||||
grid-gap: 0.5rem;
|
|
||||||
font-family: var(--mono-font);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
background-color: var(--color-code-bg);
|
|
||||||
width: fit-content;
|
|
||||||
border-radius: 10px;
|
|
||||||
user-select: text;
|
|
||||||
transition:
|
|
||||||
opacity 0.5s ease-in-out,
|
|
||||||
filter 0.2s ease-in-out,
|
|
||||||
transform 0.05s ease-in-out,
|
|
||||||
outline 0.2s ease-in-out;
|
|
||||||
|
|
||||||
span {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
filter: brightness(0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
filter: brightness(0.8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -654,11 +654,11 @@ For a brief rundown of how this works:
|
|||||||
{
|
{
|
||||||
name: "Insufficient",
|
name: "Insufficient",
|
||||||
resultingMessage: `## Insufficient Gallery Images
|
resultingMessage: `## Insufficient Gallery Images
|
||||||
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations).
|
We ask that projects like yours show off their content using images in the Gallery, or optionally in the Description, in order to effectively and clearly inform users of its content per section 2.1 of [Modrinth's content rules](https://modrinth.com/legal/rules#general-expectations).
|
||||||
Keep in mind that you should:
|
Keep in mind that you should:
|
||||||
- Set a featured image that best represents your project.
|
- Set a featured image that best represents your project.
|
||||||
- Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description.
|
- Ensure all your images have titles that accurately label the image, and optionally, details on the contents of the image in the images Description.
|
||||||
- Upload any relevant images in your Description to your Gallery tab for best results.`,
|
- Upload any relevant images in your Description to your Gallery tab for best results.`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Not relevant",
|
name: "Not relevant",
|
||||||
|
|||||||
@@ -104,13 +104,13 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
<template v-if="tags.rejectedStatuses.includes(notification.body.new_status)">
|
||||||
has been
|
has been
|
||||||
<Badge :type="notification.body.new_status" />
|
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
updated from
|
updated from
|
||||||
<Badge :type="notification.body.old_status" />
|
<ProjectStatusBadge :status="notification.body.old_status" />
|
||||||
to
|
to
|
||||||
<Badge :type="notification.body.new_status" />
|
<ProjectStatusBadge :status="notification.body.new_status" />
|
||||||
</template>
|
</template>
|
||||||
by the moderators.
|
by the moderators.
|
||||||
</template>
|
</template>
|
||||||
@@ -331,16 +331,13 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { useRelativeTime } from "@modrinth/ui";
|
import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
||||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||||
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
||||||
import { getUserLink } from "~/helpers/users.js";
|
import { getUserLink } from "~/helpers/users.js";
|
||||||
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
||||||
import { markAsRead } from "~/helpers/notifications.ts";
|
import { markAsRead } from "~/helpers/notifications.ts";
|
||||||
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
|
||||||
import Categories from "~/components/ui/search/Categories.vue";
|
import Categories from "~/components/ui/search/Categories.vue";
|
||||||
|
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
|
|||||||
128
apps/frontend/src/components/ui/OptionGroup.vue
Normal file
128
apps/frontend/src/components/ui/OptionGroup.vue
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
ref="scrollContainer"
|
||||||
|
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="`option-group-${index}`"
|
||||||
|
ref="optionButtons"
|
||||||
|
class="button-animation z-[1] flex flex-row items-center gap-2 rounded-full bg-transparent px-4 py-2 font-semibold"
|
||||||
|
:class="{
|
||||||
|
'text-button-textSelected': modelValue === option,
|
||||||
|
'text-primary': modelValue !== option,
|
||||||
|
}"
|
||||||
|
@click="setOption(option)"
|
||||||
|
>
|
||||||
|
<slot :option="option" :selected="modelValue === option" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
class="navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full bg-button-bgSelected p-1"
|
||||||
|
:style="{
|
||||||
|
left: sliderLeftPx,
|
||||||
|
top: sliderTopPx,
|
||||||
|
right: sliderRightPx,
|
||||||
|
bottom: sliderBottomPx,
|
||||||
|
opacity: initialized ? 1 : 0,
|
||||||
|
}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T">
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
|
||||||
|
const modelValue = defineModel<T>({ required: true });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
options: T[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const scrollContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const sliderLeft = ref(4);
|
||||||
|
const sliderTop = ref(4);
|
||||||
|
const sliderRight = ref(4);
|
||||||
|
const sliderBottom = ref(4);
|
||||||
|
|
||||||
|
const sliderLeftPx = computed(() => `${sliderLeft.value}px`);
|
||||||
|
const sliderTopPx = computed(() => `${sliderTop.value}px`);
|
||||||
|
const sliderRightPx = computed(() => `${sliderRight.value}px`);
|
||||||
|
const sliderBottomPx = computed(() => `${sliderBottom.value}px`);
|
||||||
|
|
||||||
|
const optionButtons = ref();
|
||||||
|
|
||||||
|
const initialized = ref(false);
|
||||||
|
|
||||||
|
function setOption(option: T) {
|
||||||
|
modelValue.value = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(modelValue, () => {
|
||||||
|
startAnimation(props.options.indexOf(modelValue.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
function startAnimation(index: number) {
|
||||||
|
const el = optionButtons.value[index];
|
||||||
|
|
||||||
|
if (!el || !el.offsetParent) return;
|
||||||
|
|
||||||
|
const newValues = {
|
||||||
|
left: el.offsetLeft,
|
||||||
|
top: el.offsetTop,
|
||||||
|
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||||
|
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
||||||
|
sliderLeft.value = newValues.left;
|
||||||
|
sliderRight.value = newValues.right;
|
||||||
|
sliderTop.value = newValues.top;
|
||||||
|
sliderBottom.value = newValues.bottom;
|
||||||
|
} else {
|
||||||
|
const delay = 200;
|
||||||
|
|
||||||
|
if (newValues.left < sliderLeft.value) {
|
||||||
|
sliderLeft.value = newValues.left;
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderRight.value = newValues.right;
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
sliderRight.value = newValues.right;
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderLeft.value = newValues.left;
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValues.top < sliderTop.value) {
|
||||||
|
sliderTop.value = newValues.top;
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderBottom.value = newValues.bottom;
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
sliderBottom.value = newValues.bottom;
|
||||||
|
setTimeout(() => {
|
||||||
|
sliderTop.value = newValues.top;
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initialized.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startAnimation(props.options.indexOf(modelValue.value));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.navtabs-transition {
|
||||||
|
transition:
|
||||||
|
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shadow {
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="count > 1" class="columns paginates">
|
|
||||||
<a
|
|
||||||
:class="{ disabled: page === 1 }"
|
|
||||||
:tabindex="page === 1 ? -1 : 0"
|
|
||||||
class="left-arrow paginate has-icon"
|
|
||||||
aria-label="Previous Page"
|
|
||||||
:href="linkFunction(page - 1)"
|
|
||||||
@click.prevent="page !== 1 ? switchPage(page - 1) : null"
|
|
||||||
>
|
|
||||||
<LeftArrowIcon />
|
|
||||||
</a>
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in pages"
|
|
||||||
:key="'page-' + item + '-' + index"
|
|
||||||
:class="{
|
|
||||||
'page-number': page !== item,
|
|
||||||
shrink: item > 99,
|
|
||||||
}"
|
|
||||||
class="page-number-container"
|
|
||||||
>
|
|
||||||
<div v-if="item === '-'" class="has-icon">
|
|
||||||
<GapIcon />
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
v-else
|
|
||||||
:class="{
|
|
||||||
'page-number current': page === item,
|
|
||||||
shrink: item > 99,
|
|
||||||
}"
|
|
||||||
:href="linkFunction(item)"
|
|
||||||
@click.prevent="page !== item ? switchPage(item) : null"
|
|
||||||
>
|
|
||||||
{{ item }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
:class="{
|
|
||||||
disabled: page === pages[pages.length - 1],
|
|
||||||
}"
|
|
||||||
:tabindex="page === pages[pages.length - 1] ? -1 : 0"
|
|
||||||
class="right-arrow paginate has-icon"
|
|
||||||
aria-label="Next Page"
|
|
||||||
:href="linkFunction(page + 1)"
|
|
||||||
@click.prevent="page !== pages[pages.length - 1] ? switchPage(page + 1) : null"
|
|
||||||
>
|
|
||||||
<RightArrowIcon />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { GapIcon, LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
GapIcon,
|
|
||||||
LeftArrowIcon,
|
|
||||||
RightArrowIcon,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
page: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
count: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
linkFunction: {
|
|
||||||
type: Function,
|
|
||||||
default() {
|
|
||||||
return () => "/";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["switch-page"],
|
|
||||||
computed: {
|
|
||||||
pages() {
|
|
||||||
let pages = [];
|
|
||||||
|
|
||||||
if (this.count > 7) {
|
|
||||||
if (this.page + 3 >= this.count) {
|
|
||||||
pages = [
|
|
||||||
1,
|
|
||||||
"-",
|
|
||||||
this.count - 4,
|
|
||||||
this.count - 3,
|
|
||||||
this.count - 2,
|
|
||||||
this.count - 1,
|
|
||||||
this.count,
|
|
||||||
];
|
|
||||||
} else if (this.page > 5) {
|
|
||||||
pages = [1, "-", this.page - 1, this.page, this.page + 1, "-", this.count];
|
|
||||||
} else {
|
|
||||||
pages = [1, 2, 3, 4, 5, "-", this.count];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pages = Array.from({ length: this.count }, (_, i) => i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
switchPage(newPage) {
|
|
||||||
this.$emit("switch-page", newPage);
|
|
||||||
if (newPage !== null && newPage !== "" && !isNaN(newPage)) {
|
|
||||||
this.$emit("switch-page", Math.min(Math.max(newPage, 1), this.count));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
a {
|
|
||||||
position: relative;
|
|
||||||
color: var(--color-button-text);
|
|
||||||
box-shadow: var(--shadow-raised), var(--shadow-inset);
|
|
||||||
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 2rem;
|
|
||||||
background: var(--color-raised-bg);
|
|
||||||
|
|
||||||
transition:
|
|
||||||
opacity 0.5s ease-in-out,
|
|
||||||
filter 0.2s ease-in-out,
|
|
||||||
transform 0.05s ease-in-out,
|
|
||||||
outline 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&.page-number.current {
|
|
||||||
background: var(--color-brand);
|
|
||||||
color: var(--color-brand-inverted);
|
|
||||||
cursor: default;
|
|
||||||
outline: 2px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.paginate.disabled {
|
|
||||||
background-color: transparent;
|
|
||||||
cursor: not-allowed;
|
|
||||||
filter: grayscale(50%);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-number-container,
|
|
||||||
a,
|
|
||||||
.has-icon {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginates {
|
|
||||||
height: 2em;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
|
|
||||||
> div,
|
|
||||||
.has-icon {
|
|
||||||
margin: 0 0.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-arrow {
|
|
||||||
margin-left: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-arrow {
|
|
||||||
margin-right: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 400px) {
|
|
||||||
.paginates {
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 530px) {
|
|
||||||
a {
|
|
||||||
width: 2.5rem;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
{{ author }}
|
{{ author }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<Badge v-if="status && status !== 'approved'" :type="status" class="status" />
|
<ProjectStatusBadge v-if="status && status !== 'approved'" :status="status" class="status" />
|
||||||
</div>
|
</div>
|
||||||
<p class="description">
|
<p class="description">
|
||||||
{{ description }}
|
{{ description }}
|
||||||
@@ -91,18 +91,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets";
|
import { CalendarIcon, UpdatedIcon, DownloadIcon, HeartIcon } from "@modrinth/assets";
|
||||||
|
import { Avatar, ProjectStatusBadge, useRelativeTime } from "@modrinth/ui";
|
||||||
import Categories from "~/components/ui/search/Categories.vue";
|
import Categories from "~/components/ui/search/Categories.vue";
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
import EnvironmentIndicator from "~/components/ui/EnvironmentIndicator.vue";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import { useRelativeTime } from "@modrinth/ui";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ProjectStatusBadge,
|
||||||
EnvironmentIndicator,
|
EnvironmentIndicator,
|
||||||
Avatar,
|
Avatar,
|
||||||
Categories,
|
Categories,
|
||||||
Badge,
|
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function generateTooltip({ series, seriesIndex, dataPointIndex, w }, props) {
|
|||||||
props,
|
props,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const returnTopN = 5;
|
const returnTopN = 15;
|
||||||
|
|
||||||
const listEntries = series
|
const listEntries = series
|
||||||
.map((value, index) => [
|
.map((value, index) => [
|
||||||
|
|||||||
@@ -104,12 +104,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
||||||
|
import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
||||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||||
import { useRelativeTime } from "@modrinth/ui";
|
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime();
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||||
import { addReportMessage } from "~/helpers/threads.js";
|
import { addReportMessage } from "~/helpers/threads.js";
|
||||||
|
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
moderation: {
|
moderation: {
|
||||||
@@ -53,13 +54,13 @@ const threadIds = [
|
|||||||
|
|
||||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||||
useBaseFetch(`users?ids=${encodeURIComponent(JSON.stringify(userIds))}`),
|
fetchSegmented(userIds, (ids) => `users?ids=${asEncodedJsonArray(ids)}`),
|
||||||
),
|
),
|
||||||
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
await useAsyncData(`versions?ids=${JSON.stringify(versionIds)}`, () =>
|
||||||
useBaseFetch(`versions?ids=${encodeURIComponent(JSON.stringify(versionIds))}`),
|
fetchSegmented(versionIds, (ids) => `versions?ids=${asEncodedJsonArray(ids)}`),
|
||||||
),
|
),
|
||||||
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
await useAsyncData(`threads?ids=${JSON.stringify(threadIds)}`, () =>
|
||||||
useBaseFetch(`threads?ids=${encodeURIComponent(JSON.stringify(threadIds))}`),
|
fetchSegmented(threadIds, (ids) => `threads?ids=${asEncodedJsonArray(ids)}`),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ const versionProjects = versions.value.map((version) => version.project_id);
|
|||||||
const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
|
const projectIds = [...new Set(reportedProjects.concat(versionProjects))];
|
||||||
|
|
||||||
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
const { data: projects } = await useAsyncData(`projects?ids=${JSON.stringify(projectIds)}`, () =>
|
||||||
useBaseFetch(`projects?ids=${encodeURIComponent(JSON.stringify(projectIds))}`),
|
fetchSegmented(projectIds, (ids) => `projects?ids=${asEncodedJsonArray(ids)}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
reports.value = rawReports.map((report) => {
|
reports.value = rawReports.map((report) => {
|
||||||
|
|||||||
@@ -45,9 +45,11 @@
|
|||||||
import { ref, nextTick, computed } from "vue";
|
import { ref, nextTick, computed } from "vue";
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
|
import { IssuesIcon, PlusIcon, XIcon } from "@modrinth/assets";
|
||||||
|
import { ModrinthServersFetchError, type ServerBackup } from "@modrinth/utils";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
@@ -64,7 +66,7 @@ const trimmedName = computed(() => backupName.value.trim());
|
|||||||
const nameExists = computed(() => {
|
const nameExists = computed(() => {
|
||||||
if (!props.server.backups?.data) return false;
|
if (!props.server.backups?.data) return false;
|
||||||
return props.server.backups.data.some(
|
return props.server.backups.data.some(
|
||||||
(backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup: ServerBackup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ const createBackup = async () => {
|
|||||||
hideModal();
|
hideModal();
|
||||||
await props.server.refresh();
|
await props.server.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||||
isRateLimited.value = true;
|
isRateLimited.value = true;
|
||||||
addNotification({
|
addNotification({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -20,13 +20,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { ConfirmModal } from "@modrinth/ui";
|
import { ConfirmModal } from "@modrinth/ui";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import type { Backup } from "@modrinth/utils";
|
||||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "delete", backup: Backup | undefined): void;
|
(e: "delete", backup: Backup | undefined): void;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
||||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { Backup } from "~/composables/pyroServers.ts";
|
import type { Backup } from "@modrinth/utils";
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags();
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|||||||
@@ -48,10 +48,11 @@
|
|||||||
import { ref, nextTick, computed } from "vue";
|
import { ref, nextTick, computed } from "vue";
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
import { SpinnerIcon, SaveIcon, XIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import type { Backup } from "@modrinth/utils";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
@@ -70,7 +71,7 @@ const nameExists = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return props.server.backups.data.some(
|
return props.server.backups.data.some(
|
||||||
(backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
(backup: Backup) => backup.name.trim().toLowerCase() === trimmedName.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { ConfirmModal, NewModal } from "@modrinth/ui";
|
import { ConfirmModal, NewModal } from "@modrinth/ui";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import type { Backup } from "@modrinth/utils";
|
||||||
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
import BackupItem from "~/components/ui/servers/BackupItem.vue";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
|
|||||||
@@ -59,10 +59,10 @@
|
|||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { XIcon, SaveIcon } from "@modrinth/assets";
|
import { XIcon, SaveIcon } from "@modrinth/assets";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["backups"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
const modal = ref<InstanceType<typeof NewModal>>();
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ import {
|
|||||||
import { Admonition, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { Admonition, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
|
import TagItem from "@modrinth/ui/src/components/base/TagItem.vue";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import { formatCategory, formatVersionsForDisplay, type Version } from "@modrinth/utils";
|
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from "@modrinth/utils";
|
||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import Accordion from "~/components/ui/Accordion.vue";
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||||
import ContentVersionFilter, {
|
import ContentVersionFilter, {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
|
import { FolderOpenIcon, CheckCircleIcon, XCircleIcon } from "@modrinth/assets";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { ref, computed, watch, nextTick } from "vue";
|
import { ref, computed, watch, nextTick } from "vue";
|
||||||
import type { FSModule } from "~/composables/pyroServers.ts";
|
import { FSModule } from "~/composables/servers/modules/fs.ts";
|
||||||
|
|
||||||
interface UploadItem {
|
interface UploadItem {
|
||||||
file: File;
|
file: File;
|
||||||
|
|||||||
@@ -75,13 +75,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ExternalIcon, SpinnerIcon, DownloadIcon, XIcon } from "@modrinth/assets";
|
import { ExternalIcon, SpinnerIcon, DownloadIcon, XIcon } from "@modrinth/assets";
|
||||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
|
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
import { ref, computed, nextTick } from "vue";
|
import { ref, computed, nextTick } from "vue";
|
||||||
import { handleError, type Server } from "~/composables/pyroServers.ts";
|
import { handleError, ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const cf = ref(false);
|
const cf = ref(false);
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<typeof NewModal>();
|
const modal = ref<typeof NewModal>();
|
||||||
@@ -110,24 +111,18 @@ const handleSubmit = async () => {
|
|||||||
if (!error.value) {
|
if (!error.value) {
|
||||||
// hide();
|
// hide();
|
||||||
try {
|
try {
|
||||||
const dry = await props.server.fs?.extractFile(trimmedUrl.value, true, true);
|
const dry = await props.server.fs.extractFile(trimmedUrl.value, true, true);
|
||||||
|
|
||||||
if (!cf.value || dry.modpack_name) {
|
if (!cf.value || dry.modpack_name) {
|
||||||
await props.server.fs?.extractFile(trimmedUrl.value, true, false, true);
|
await props.server.fs.extractFile(trimmedUrl.value, true, false, true);
|
||||||
hide();
|
hide();
|
||||||
} else {
|
} else {
|
||||||
submitted.value = false;
|
submitted.value = false;
|
||||||
handleError(
|
handleError(
|
||||||
new ServersError(
|
new ModrinthServersFetchError(
|
||||||
"Could not find CurseForge modpack at that URL.",
|
"Could not find CurseForge modpack at that URL.",
|
||||||
undefined,
|
404,
|
||||||
undefined,
|
new Error(`No modpack found at ${url.value}`),
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
context: "Error installing modpack",
|
|
||||||
error: `url: ${url.value}`,
|
|
||||||
description: "Could not find CurseForge modpack at that URL.",
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const props = defineProps<{
|
|||||||
loader: string | null;
|
loader: string | null;
|
||||||
loader_version: string | null;
|
loader_version: string | null;
|
||||||
};
|
};
|
||||||
|
ignoreCurrentInstallation?: boolean;
|
||||||
isInstalling?: boolean;
|
isInstalling?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
style="font-variant-numeric: tabular-nums"
|
|
||||||
class="pointer-events-none h-full w-full select-none"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-6">
|
|
||||||
<div class="flex flex-row items-center gap-6">
|
|
||||||
<div
|
|
||||||
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
|
||||||
>
|
|
||||||
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
|
|
||||||
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
|
|
||||||
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
|
|
||||||
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
|
|
||||||
</div>
|
|
||||||
<h3 class="relative z-10 text-base font-normal text-secondary">CPU usage</h3>
|
|
||||||
</div>
|
|
||||||
<CPUIcon class="absolute right-10 top-10" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative max-h-[156px] min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
|
||||||
>
|
|
||||||
<div class="relative z-10 -ml-3 w-fit rounded-xl px-3 py-1">
|
|
||||||
<div class="-mb-0.5 mt-0.5 flex flex-row items-center gap-2">
|
|
||||||
<h2 class="m-0 -ml-0.5 text-3xl font-extrabold text-contrast">0.00%</h2>
|
|
||||||
<h3 class="relative z-10 text-sm font-normal text-secondary">/ 100%</h3>
|
|
||||||
</div>
|
|
||||||
<h3 class="relative z-10 text-base font-normal text-secondary">Memory usage</h3>
|
|
||||||
</div>
|
|
||||||
<DBIcon class="absolute right-10 top-10" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100"
|
|
||||||
>
|
|
||||||
<div class="flex flex-row items-center gap-2">
|
|
||||||
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">0 B</h2>
|
|
||||||
</div>
|
|
||||||
<h3 class="relative z-10 text-base font-normal text-secondary">Storage usage</h3>
|
|
||||||
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="relative flex h-full w-full flex-col gap-3 overflow-hidden rounded-2xl bg-bg-raised p-8"
|
|
||||||
>
|
|
||||||
<div class="experimental-styles-within flex flex-row items-center">
|
|
||||||
<div class="flex flex-row items-center gap-4">
|
|
||||||
<h2 class="m-0 text-3xl font-extrabold text-contrast">Console</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="relative w-full">
|
|
||||||
<input type="text" placeholder="Search logs" class="h-12 !w-full !pl-10 !pr-48" />
|
|
||||||
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="console relative h-full min-h-[516px] w-full overflow-hidden rounded-xl bg-bg text-sm"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { CPUIcon, DBIcon, FolderOpenIcon, SearchIcon } from "@modrinth/assets";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
html.light-mode .console {
|
|
||||||
background: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark-mode .console {
|
|
||||||
background: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.oled-mode .console {
|
|
||||||
background: black;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ButtonStyled v-if="showStopButton" type="transparent">
|
<ButtonStyled v-if="showStopButton" type="transparent">
|
||||||
<button :disabled="!canTakeAction" @click="initiateAction('stop')">
|
<button :disabled="!canTakeAction" @click="initiateAction('Stop')">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<StopCircleIcon class="h-5 w-5" />
|
<StopCircleIcon class="h-5 w-5" />
|
||||||
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
<span>{{ isStoppingState ? "Stopping..." : "Stop" }}</span>
|
||||||
@@ -120,14 +120,12 @@ import {
|
|||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
|
import type { PowerAction as ServerPowerAction, ServerState } from "@modrinth/utils";
|
||||||
type ServerAction = "start" | "stop" | "restart" | "kill";
|
|
||||||
type ServerState = "stopped" | "starting" | "running" | "stopping" | "restarting";
|
|
||||||
|
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
interface PowerAction {
|
interface PowerAction {
|
||||||
action: ServerAction;
|
action: ServerPowerAction;
|
||||||
nextState: ServerState;
|
nextState: ServerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +140,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "action", action: ServerAction): void;
|
(e: "action", action: ServerPowerAction): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -170,7 +168,7 @@ const isStoppingState = computed(() => serverState.value === "stopping");
|
|||||||
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
const showStopButton = computed(() => isRunning.value || isStoppingState.value);
|
||||||
|
|
||||||
const primaryActionText = computed(() => {
|
const primaryActionText = computed(() => {
|
||||||
const states: Record<ServerState, string> = {
|
const states: Partial<Record<ServerState, string>> = {
|
||||||
starting: "Starting...",
|
starting: "Starting...",
|
||||||
restarting: "Restarting...",
|
restarting: "Restarting...",
|
||||||
running: "Restart",
|
running: "Restart",
|
||||||
@@ -193,7 +191,7 @@ const menuOptions = computed(() => [
|
|||||||
id: "kill",
|
id: "kill",
|
||||||
label: "Kill server",
|
label: "Kill server",
|
||||||
icon: SlashIcon,
|
icon: SlashIcon,
|
||||||
action: () => initiateAction("kill"),
|
action: () => initiateAction("Kill"),
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
{
|
{
|
||||||
@@ -221,17 +219,17 @@ async function copyId() {
|
|||||||
await navigator.clipboard.writeText(serverId as string);
|
await navigator.clipboard.writeText(serverId as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initiateAction(action: ServerAction) {
|
function initiateAction(action: ServerPowerAction) {
|
||||||
if (!canTakeAction.value) return;
|
if (!canTakeAction.value) return;
|
||||||
|
|
||||||
const stateMap: Record<ServerAction, ServerState> = {
|
const stateMap: Record<ServerPowerAction, ServerState> = {
|
||||||
start: "starting",
|
Start: "starting",
|
||||||
stop: "stopping",
|
Stop: "stopping",
|
||||||
restart: "restarting",
|
Restart: "restarting",
|
||||||
kill: "stopping",
|
Kill: "stopping",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (action === "start") {
|
if (action === "Start") {
|
||||||
emit("action", action);
|
emit("action", action);
|
||||||
serverState.value = stateMap[action];
|
serverState.value = stateMap[action];
|
||||||
startingDelay.value = true;
|
startingDelay.value = true;
|
||||||
@@ -249,7 +247,7 @@ function initiateAction(action: ServerAction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handlePrimaryAction() {
|
function handlePrimaryAction() {
|
||||||
initiateAction(isRunning.value ? "restart" : "start");
|
initiateAction(isRunning.value ? "Restart" : "Start");
|
||||||
}
|
}
|
||||||
|
|
||||||
function executePowerAction() {
|
function executePowerAction() {
|
||||||
@@ -263,7 +261,7 @@ function executePowerAction() {
|
|||||||
userPreferences.value.powerDontAskAgain = true;
|
userPreferences.value.powerDontAskAgain = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "start") {
|
if (action === "Start") {
|
||||||
startingDelay.value = true;
|
startingDelay.value = true;
|
||||||
setTimeout(() => (startingDelay.value = false), 5000);
|
setTimeout(() => (startingDelay.value = false), 5000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { ServerState } from "~/types/servers";
|
import type { ServerState } from "@modrinth/utils";
|
||||||
|
|
||||||
const STATUS_CLASSES = {
|
const STATUS_CLASSES = {
|
||||||
running: { main: "bg-brand", bg: "bg-bg-green" },
|
running: { main: "bg-brand", bg: "bg-bg-green" },
|
||||||
@@ -49,7 +49,7 @@ const STATUS_CLASSES = {
|
|||||||
unknown: { main: "", bg: "" },
|
unknown: { main: "", bg: "" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const STATUS_TEXTS = {
|
const STATUS_TEXTS: Partial<Record<ServerState, string>> = {
|
||||||
running: "Running",
|
running: "Running",
|
||||||
stopped: "",
|
stopped: "",
|
||||||
crashed: "Crashed",
|
crashed: "Crashed",
|
||||||
@@ -63,7 +63,10 @@ defineProps<{
|
|||||||
const isExpanded = ref(false);
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
function getStatusClass(state: ServerState) {
|
function getStatusClass(state: ServerState) {
|
||||||
return STATUS_CLASSES[state] ?? STATUS_CLASSES.unknown;
|
if (state in STATUS_CLASSES) {
|
||||||
|
return STATUS_CLASSES[state as keyof typeof STATUS_CLASSES];
|
||||||
|
}
|
||||||
|
return STATUS_CLASSES.unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusText(state: ServerState) {
|
function getStatusText(state: ServerState) {
|
||||||
|
|||||||
@@ -7,16 +7,17 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search logs"
|
placeholder="Search logs"
|
||||||
class="h-12 !w-full !pl-10 !pr-48"
|
class="h-12 !w-full !pl-10 !pr-48"
|
||||||
|
:disabled="loading"
|
||||||
@keydown.escape="clearSearch"
|
@keydown.escape="clearSearch"
|
||||||
/>
|
/>
|
||||||
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
|
<SearchIcon class="absolute left-4 top-1/2 -translate-y-1/2" />
|
||||||
<ButtonStyled v-if="searchInput" @click="clearSearch">
|
<ButtonStyled v-if="searchInput && !loading" @click="clearSearch">
|
||||||
<button class="absolute right-2 top-1/2 -translate-y-1/2">
|
<button class="absolute right-2 top-1/2 -translate-y-1/2">
|
||||||
<XIcon class="h-5 w-5" />
|
<XIcon class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<span
|
<span
|
||||||
v-if="pyroConsole.filteredOutput.value.length && searchInput"
|
v-if="pyroConsole.filteredOutput.value.length && searchInput && !loading"
|
||||||
class="pointer-events-none absolute right-12 top-1/2 -translate-y-1/2 select-none whitespace-pre text-sm"
|
class="pointer-events-none absolute right-12 top-1/2 -translate-y-1/2 select-none whitespace-pre text-sm"
|
||||||
>
|
>
|
||||||
{{ pyroConsole.filteredOutput.value.length }}
|
{{ pyroConsole.filteredOutput.value.length }}
|
||||||
@@ -29,11 +30,13 @@
|
|||||||
:class="[
|
:class="[
|
||||||
'terminal-font console relative z-[1] flex h-full w-full flex-col items-center justify-between overflow-hidden rounded-t-xl px-1 text-sm transition-transform duration-300',
|
'terminal-font console relative z-[1] flex h-full w-full flex-col items-center justify-between overflow-hidden rounded-t-xl px-1 text-sm transition-transform duration-300',
|
||||||
{ 'scale-fullscreen screen-fixed inset-0 z-50 !rounded-none': isFullScreen },
|
{ 'scale-fullscreen screen-fixed inset-0 z-50 !rounded-none': isFullScreen },
|
||||||
|
{ 'pointer-events-none': loading },
|
||||||
]"
|
]"
|
||||||
|
:aria-hidden="loading"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="cosmetics.advancedRendering"
|
v-if="cosmetics.advancedRendering && !loading"
|
||||||
class="progressive-gradient pointer-events-none absolute -bottom-6 left-0 z-[2] h-[10rem] w-full overflow-hidden rounded-xl"
|
class="progressive-gradient pointer-events-none absolute -bottom-6 left-0 z-[2] h-[10rem] w-full overflow-hidden rounded-xl"
|
||||||
:style="`--transparency: ${Math.max(0, lerp(100, 0, bottomThreshold * 8))}%`"
|
:style="`--transparency: ${Math.max(0, lerp(100, 0, bottomThreshold * 8))}%`"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="!loading"
|
||||||
class="pointer-events-none absolute bottom-0 left-0 right-0 z-[2] h-[196px] w-full"
|
class="pointer-events-none absolute bottom-0 left-0 right-0 z-[2] h-[196px] w-full"
|
||||||
:style="
|
:style="
|
||||||
bottomThreshold > 0
|
bottomThreshold > 0
|
||||||
@@ -79,6 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div data-pyro-terminal-scroll-root class="relative h-full w-full">
|
<div data-pyro-terminal-scroll-root class="relative h-full w-full">
|
||||||
<div
|
<div
|
||||||
|
v-if="!loading"
|
||||||
ref="scrollbarTrack"
|
ref="scrollbarTrack"
|
||||||
data-pyro-terminal-scrollbar-track
|
data-pyro-terminal-scrollbar-track
|
||||||
class="absolute -right-1 bottom-16 top-4 z-[4] w-4 overflow-hidden"
|
class="absolute -right-1 bottom-16 top-4 z-[4] w-4 overflow-hidden"
|
||||||
@@ -118,7 +122,12 @@
|
|||||||
class="scrollbar-none absolute left-0 top-0 h-full w-full select-text overflow-x-auto overflow-y-auto py-6 pb-[72px]"
|
class="scrollbar-none absolute left-0 top-0 h-full w-full select-text overflow-x-auto overflow-y-auto py-6 pb-[72px]"
|
||||||
@scroll.passive="() => handleListScroll()"
|
@scroll.passive="() => handleListScroll()"
|
||||||
>
|
>
|
||||||
<div data-pyro-terminal-virtual-height-watcher :style="{ height: `${totalHeight}px` }">
|
<div v-if="loading" class="h-full w-full" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
data-pyro-terminal-virtual-height-watcher
|
||||||
|
:style="{ height: `${totalHeight}px` }"
|
||||||
|
>
|
||||||
<ul
|
<ul
|
||||||
class="m-0 list-none p-0"
|
class="m-0 list-none p-0"
|
||||||
data-pyro-terminal-virtual-list
|
data-pyro-terminal-virtual-list
|
||||||
@@ -205,6 +214,7 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
v-if="!loading"
|
||||||
data-pyro-fullscreen
|
data-pyro-fullscreen
|
||||||
:label="isFullScreen ? 'Exit full screen' : 'Enter full screen'"
|
:label="isFullScreen ? 'Exit full screen' : 'Enter full screen'"
|
||||||
class="experimental-styles-within absolute right-4 top-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
class="experimental-styles-within absolute right-4 top-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
||||||
@@ -217,7 +227,7 @@
|
|||||||
|
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-if="hasSelection || isSingleLineSelected"
|
v-if="(hasSelection || isSingleLineSelected) && !loading"
|
||||||
class="absolute right-20 top-4 z-[3] flex flex-row items-center"
|
class="absolute right-20 top-4 z-[3] flex flex-row items-center"
|
||||||
:class="{ '!right-4': searchInput || hasSelection || isSingleLineSelected }"
|
:class="{ '!right-4': searchInput || hasSelection || isSingleLineSelected }"
|
||||||
>
|
>
|
||||||
@@ -247,7 +257,7 @@
|
|||||||
|
|
||||||
<Transition name="scroll-to-bottom">
|
<Transition name="scroll-to-bottom">
|
||||||
<button
|
<button
|
||||||
v-if="bottomThreshold > 0 && !isScrolledToBottom"
|
v-if="bottomThreshold > 0 && !isScrolledToBottom && !loading"
|
||||||
data-pyro-scrolltobottom
|
data-pyro-scrolltobottom
|
||||||
label="Scroll to bottom"
|
label="Scroll to bottom"
|
||||||
class="scroll-to-bottom-btn experimental-styles-within absolute bottom-[4.5rem] right-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
class="scroll-to-bottom-btn experimental-styles-within absolute bottom-[4.5rem] right-4 z-[3] grid h-12 w-12 place-content-center rounded-full border-[1px] border-solid border-button-border bg-bg-raised text-contrast transition-all duration-200 hover:scale-110 active:scale-95"
|
||||||
@@ -291,13 +301,14 @@ import { useDebounceFn } from "@vueuse/core";
|
|||||||
import { NewModal } from "@modrinth/ui";
|
import { NewModal } from "@modrinth/ui";
|
||||||
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
import ButtonStyled from "@modrinth/ui/src/components/base/ButtonStyled.vue";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { usePyroConsole } from "~/store/console.ts";
|
import { useModrinthServersConsole } from "~/store/console.ts";
|
||||||
|
|
||||||
const { $cosmetics } = useNuxtApp();
|
const { $cosmetics } = useNuxtApp();
|
||||||
const cosmetics = $cosmetics;
|
const cosmetics = $cosmetics;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
|
loading?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const BUFFER_SIZE = 5;
|
const BUFFER_SIZE = 5;
|
||||||
@@ -307,8 +318,8 @@ const SEPARATOR_HEIGHT = 32;
|
|||||||
const SCROLL_END_DELAY = 150;
|
const SCROLL_END_DELAY = 150;
|
||||||
const progressiveBlurIterations = ref(8);
|
const progressiveBlurIterations = ref(8);
|
||||||
|
|
||||||
const pyroConsole = usePyroConsole();
|
const pyroConsole = useModrinthServersConsole();
|
||||||
const consoleOutput = pyroConsole.output;
|
const consoleOutput = computed(() => (props.loading ? [] : pyroConsole.output.value));
|
||||||
|
|
||||||
const scrollContainer = ref<HTMLElement | null>(null);
|
const scrollContainer = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -69,10 +69,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { DownloadIcon, XIcon } from "@modrinth/assets";
|
import { DownloadIcon, XIcon } from "@modrinth/assets";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
project: any;
|
project: any;
|
||||||
versions: any[];
|
versions: any[];
|
||||||
currentVersion?: any;
|
currentVersion?: any;
|
||||||
@@ -98,8 +99,7 @@ const handleReinstall = async () => {
|
|||||||
try {
|
try {
|
||||||
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id;
|
const versionId = props.versions.find((v) => v.version_number === selectedVersion.value)?.id;
|
||||||
|
|
||||||
await props.server.general?.reinstall(
|
await props.server.general.reinstall(
|
||||||
props.server.serverId,
|
|
||||||
false,
|
false,
|
||||||
props.project.id,
|
props.project.id,
|
||||||
versionId,
|
versionId,
|
||||||
@@ -110,7 +110,7 @@ const handleReinstall = async () => {
|
|||||||
emit("reinstall");
|
emit("reinstall");
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -116,11 +116,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { UploadIcon, RightArrowIcon, XIcon, ServerIcon } from "@modrinth/assets";
|
import { UploadIcon, RightArrowIcon, XIcon, ServerIcon } from "@modrinth/assets";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ const handleReinstall = async () => {
|
|||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && error?.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -127,7 +127,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
<div
|
||||||
|
v-if="!initialSetup"
|
||||||
|
class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4"
|
||||||
|
>
|
||||||
<div class="flex w-full flex-row items-center justify-between">
|
<div class="flex w-full flex-row items-center justify-between">
|
||||||
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
|
<label class="w-full text-lg font-bold text-contrast" for="hard-reset">
|
||||||
Erase all data
|
Erase all data
|
||||||
@@ -146,7 +149,10 @@
|
|||||||
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
<div class="font-bold">This does not affect your backups, which are stored off-site.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BackupWarning :backup-link="`/servers/manage/${props.server?.serverId}/backups`" />
|
<BackupWarning
|
||||||
|
v-if="!initialSetup"
|
||||||
|
:backup-link="`/servers/manage/${props.server?.serverId}/backups`"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex justify-start gap-4">
|
<div class="mt-4 flex justify-start gap-4">
|
||||||
@@ -194,9 +200,9 @@
|
|||||||
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
import { BackupWarning, ButtonStyled, NewModal, Toggle } from "@modrinth/ui";
|
||||||
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
import { DropdownIcon, RightArrowIcon, ServerIcon, XIcon } from "@modrinth/assets";
|
||||||
import { $fetch } from "ofetch";
|
import { $fetch } from "ofetch";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { type Loaders, ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
import type { Loaders } from "~/types/servers";
|
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|
||||||
@@ -214,9 +220,10 @@ type VersionMap = Record<string, LoaderVersion[]>;
|
|||||||
type VersionCache = Record<string, any>;
|
type VersionCache = Record<string, any>;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
currentLoader: Loaders | undefined;
|
currentLoader: Loaders | undefined;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
|
initialSetup?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -313,7 +320,7 @@ const selectedLoaderVersions = computed<string[]>(() => {
|
|||||||
const loader = selectedLoader.value.toLowerCase();
|
const loader = selectedLoader.value.toLowerCase();
|
||||||
|
|
||||||
if (loader === "paper") {
|
if (loader === "paper") {
|
||||||
return paperVersions.value[selectedMCVersion.value].map((x) => `${x}`) || [];
|
return paperVersions.value[selectedMCVersion.value]?.map((x) => `${x}`) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loader === "purpur") {
|
if (loader === "purpur") {
|
||||||
@@ -451,12 +458,11 @@ const handleReinstall = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await props.server.general?.reinstall(
|
await props.server.general?.reinstall(
|
||||||
props.server.serverId,
|
|
||||||
true,
|
true,
|
||||||
selectedLoader.value,
|
selectedLoader.value,
|
||||||
selectedMCVersion.value,
|
selectedMCVersion.value,
|
||||||
selectedLoader.value === "Vanilla" ? "" : selectedLoaderVersion.value,
|
selectedLoader.value === "Vanilla" ? "" : selectedLoaderVersion.value,
|
||||||
hardReset.value,
|
props.initialSetup ? true : hardReset.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
emit("reinstall", {
|
emit("reinstall", {
|
||||||
@@ -467,7 +473,7 @@ const handleReinstall = async () => {
|
|||||||
|
|
||||||
hide();
|
hide();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PyroFetchError && error.statusCode === 429) {
|
if (error instanceof ModrinthServersFetchError && (error as any)?.statusCode === 429) {
|
||||||
addNotification({
|
addNotification({
|
||||||
group: "server",
|
group: "server",
|
||||||
title: "Cannot reinstall server",
|
title: "Cannot reinstall server",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
@@ -39,7 +39,7 @@ const props = defineProps<{
|
|||||||
save: () => void;
|
save: () => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const saveAndRestart = async () => {
|
const saveAndRestart = async () => {
|
||||||
|
|||||||
278
apps/frontend/src/components/ui/servers/ServerInstallation.vue
Normal file
278
apps/frontend/src/components/ui/servers/ServerInstallation.vue
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<template>
|
||||||
|
<LazyUiServersPlatformVersionSelectModal
|
||||||
|
ref="versionSelectModal"
|
||||||
|
:server="props.server"
|
||||||
|
:current-loader="ignoreCurrentInstallation ? undefined : (data?.loader as Loaders)"
|
||||||
|
:backup-in-progress="backupInProgress"
|
||||||
|
:initial-setup="ignoreCurrentInstallation"
|
||||||
|
@reinstall="emit('reinstall', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LazyUiServersPlatformMrpackModal
|
||||||
|
ref="mrpackModal"
|
||||||
|
:server="props.server"
|
||||||
|
@reinstall="emit('reinstall', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LazyUiServersPlatformChangeModpackVersionModal
|
||||||
|
ref="modpackVersionModal"
|
||||||
|
:server="props.server"
|
||||||
|
:project="data?.project"
|
||||||
|
:versions="Array.isArray(versions) ? versions : []"
|
||||||
|
:current-version="currentVersion"
|
||||||
|
:current-version-id="data?.upstream?.version_id"
|
||||||
|
:server-status="data?.status"
|
||||||
|
@reinstall="emit('reinstall')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex h-full w-full flex-col">
|
||||||
|
<div v-if="data && versions" class="flex w-full flex-col">
|
||||||
|
<div class="card flex flex-col gap-4">
|
||||||
|
<div class="flex select-none flex-col items-center justify-between gap-2 lg:flex-row">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<h2 class="m-0 text-lg font-bold text-contrast">Modpack</h2>
|
||||||
|
<div
|
||||||
|
v-if="updateAvailable"
|
||||||
|
class="rounded-full bg-bg-orange px-2 py-1 text-xs font-medium text-orange"
|
||||||
|
>
|
||||||
|
<span>Update available</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="data.upstream" class="flex gap-4">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button
|
||||||
|
class="!w-full sm:!w-auto"
|
||||||
|
:disabled="isInstalling"
|
||||||
|
@click="mrpackModal.show()"
|
||||||
|
>
|
||||||
|
<UploadIcon class="size-4" /> Import .mrpack
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<!-- dumb hack to make a button link not a link -->
|
||||||
|
<ButtonStyled>
|
||||||
|
<template v-if="isInstalling">
|
||||||
|
<button :disabled="isInstalling">
|
||||||
|
<TransferIcon class="size-4" />
|
||||||
|
Switch modpack
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<nuxt-link v-else :to="`/modpacks?sid=${props.server.serverId}`">
|
||||||
|
<TransferIcon class="size-4" />
|
||||||
|
Switch modpack
|
||||||
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="data.upstream" class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-if="versionsError || currentVersionError"
|
||||||
|
class="rounded-2xl border border-solid border-red p-4 text-contrast"
|
||||||
|
>
|
||||||
|
<p class="m-0 font-bold">Something went wrong while loading your modpack.</p>
|
||||||
|
<p class="m-0 mb-2 mt-1 text-sm">
|
||||||
|
{{ versionsError || currentVersionError }}
|
||||||
|
</p>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button :disabled="isInstalling" @click="refreshData">Retry</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NewProjectCard
|
||||||
|
v-if="!versionsError && !currentVersionError"
|
||||||
|
class="!cursor-default !bg-bg !filter-none"
|
||||||
|
:project="projectCardData"
|
||||||
|
:categories="data.project?.categories || []"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button :disabled="isInstalling" @click="modpackVersionModal.show()">
|
||||||
|
<SettingsIcon class="size-4" />
|
||||||
|
Change version
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</NewProjectCard>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex w-full flex-col items-center gap-2 sm:w-fit sm:flex-row">
|
||||||
|
<ButtonStyled>
|
||||||
|
<nuxt-link
|
||||||
|
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||||
|
:class="{ disabled: backupInProgress }"
|
||||||
|
class="!w-full sm:!w-auto"
|
||||||
|
:to="`/modpacks?sid=${props.server.serverId}`"
|
||||||
|
>
|
||||||
|
<CompassIcon class="size-4" /> Find a modpack
|
||||||
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
<span class="hidden sm:block">or</span>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button
|
||||||
|
v-tooltip="backupInProgress ? formatMessage(backupInProgress.tooltip) : undefined"
|
||||||
|
:disabled="!!backupInProgress"
|
||||||
|
class="!w-full sm:!w-auto"
|
||||||
|
@click="mrpackModal.show()"
|
||||||
|
>
|
||||||
|
<UploadIcon class="size-4" /> Upload .mrpack file
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h2 class="m-0 text-lg font-bold text-contrast">Platform</h2>
|
||||||
|
<p class="m-0">Your server's platform is the software that runs mods and plugins.</p>
|
||||||
|
<div v-if="data.upstream" class="mt-2 flex items-center gap-2">
|
||||||
|
<InfoIcon class="hidden sm:block" />
|
||||||
|
<span class="text-sm text-secondary">
|
||||||
|
The current platform was automatically selected based on your modpack.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-col gap-1 rounded-2xl"
|
||||||
|
:class="{
|
||||||
|
'pointer-events-none cursor-not-allowed select-none opacity-50':
|
||||||
|
props.server.general?.status === 'installing',
|
||||||
|
}"
|
||||||
|
:tabindex="props.server.general?.status === 'installing' ? -1 : 0"
|
||||||
|
>
|
||||||
|
<UiServersLoaderSelector
|
||||||
|
:data="
|
||||||
|
ignoreCurrentInstallation
|
||||||
|
? {
|
||||||
|
loader: null,
|
||||||
|
loader_version: null,
|
||||||
|
}
|
||||||
|
: data
|
||||||
|
"
|
||||||
|
:is-installing="isInstalling"
|
||||||
|
@select-loader="selectLoader"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
||||||
|
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
||||||
|
import type { Loaders } from "@modrinth/utils";
|
||||||
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
|
const { formatMessage } = useVIntl();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
server: ModrinthServer;
|
||||||
|
ignoreCurrentInstallation?: boolean;
|
||||||
|
backupInProgress?: BackupInProgressReason;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
reinstall: [any?];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isInstalling = computed(() => props.server.general?.status === "installing");
|
||||||
|
|
||||||
|
const versionSelectModal = ref();
|
||||||
|
const mrpackModal = ref();
|
||||||
|
const modpackVersionModal = ref();
|
||||||
|
|
||||||
|
const data = computed(() => props.server.general);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: versions,
|
||||||
|
error: versionsError,
|
||||||
|
refresh: refreshVersions,
|
||||||
|
} = await useAsyncData(
|
||||||
|
`content-loader-versions-${data.value?.upstream?.project_id}`,
|
||||||
|
async () => {
|
||||||
|
if (!data.value?.upstream?.project_id) return [];
|
||||||
|
try {
|
||||||
|
const result = await useBaseFetch(`project/${data.value.upstream.project_id}/version`);
|
||||||
|
return result || [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error("couldnt fetch all versions:", e);
|
||||||
|
throw new Error("Failed to load modpack versions.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: currentVersion,
|
||||||
|
error: currentVersionError,
|
||||||
|
refresh: refreshCurrentVersion,
|
||||||
|
} = await useAsyncData(
|
||||||
|
`content-loader-version-${data.value?.upstream?.version_id}`,
|
||||||
|
async () => {
|
||||||
|
if (!data.value?.upstream?.version_id) return null;
|
||||||
|
try {
|
||||||
|
const result = await useBaseFetch(`version/${data.value.upstream.version_id}`);
|
||||||
|
return result || null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("couldnt fetch version:", e);
|
||||||
|
throw new Error("Failed to load modpack version.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => null },
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectCardData = computed(() => ({
|
||||||
|
icon_url: data.value?.project?.icon_url,
|
||||||
|
title: data.value?.project?.title,
|
||||||
|
description: data.value?.project?.description,
|
||||||
|
downloads: data.value?.project?.downloads,
|
||||||
|
follows: data.value?.project?.followers,
|
||||||
|
// @ts-ignore
|
||||||
|
date_modified: currentVersion.value?.date_published || data.value?.project?.updated,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectLoader = (loader: string) => {
|
||||||
|
versionSelectModal.value?.show(loader as Loaders);
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
await Promise.all([refreshVersions(), refreshCurrentVersion()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAvailable = computed(() => {
|
||||||
|
// so sorry
|
||||||
|
// @ts-ignore
|
||||||
|
if (!data.value?.upstream || !versions.value?.length || !currentVersion.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const latestVersion = versions.value[0];
|
||||||
|
// @ts-ignore
|
||||||
|
return latestVersion.id !== currentVersion.value.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.server.general?.status,
|
||||||
|
async (newStatus, oldStatus) => {
|
||||||
|
if (oldStatus === "installing" && newStatus === "available") {
|
||||||
|
await Promise.all([
|
||||||
|
refreshVersions(),
|
||||||
|
refreshCurrentVersion(),
|
||||||
|
props.server.refresh(["general"]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stylized-toggle:checked::after {
|
||||||
|
background: var(--color-accent-contrast) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-base:active {
|
||||||
|
scale: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -43,7 +43,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="min-h-[20px]"></div>
|
<div v-else class="min-h-[20px]"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isConfiguring"
|
||||||
|
class="flex min-w-0 items-center gap-2 truncate text-sm font-semibold text-brand"
|
||||||
|
>
|
||||||
|
<SparklesIcon class="size-5 shrink-0" /> New server
|
||||||
|
</div>
|
||||||
<UiServersServerInfoLabels
|
<UiServersServerInfoLabels
|
||||||
|
v-else
|
||||||
:server-data="{ game, mc_version, loader, loader_version, net }"
|
:server-data="{ game, mc_version, loader, loader_version, net }"
|
||||||
:show-game-label="showGameLabel"
|
:show-game-label="showGameLabel"
|
||||||
:show-loader-label="showLoaderLabel"
|
:show-loader-label="showLoaderLabel"
|
||||||
@@ -73,13 +80,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, HammerIcon, LockIcon } from "@modrinth/assets";
|
import { ChevronRightIcon, LockIcon, SparklesIcon } from "@modrinth/assets";
|
||||||
import type { Project, Server } from "~/types/servers";
|
import type { Project, Server } from "@modrinth/utils";
|
||||||
|
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const props = defineProps<Partial<Server>>();
|
const props = defineProps<Partial<Server>>();
|
||||||
|
|
||||||
if (props.server_id) {
|
if (props.server_id) {
|
||||||
await usePyroServer(props.server_id, ["general"]);
|
await useModrinthServers(props.server_id, ["general"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showGameLabel = computed(() => !!props.game);
|
const showGameLabel = computed(() => !!props.game);
|
||||||
@@ -102,8 +110,9 @@ if (props.upstream) {
|
|||||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined);
|
||||||
|
|
||||||
if (import.meta.server && projectData.value?.icon_url) {
|
if (import.meta.server && projectData.value?.icon_url) {
|
||||||
await usePyroServer(props.server_id!, ["general"]);
|
await useModrinthServers(props.server_id!, ["general"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
const iconUrl = computed(() => projectData.value?.icon_url || undefined);
|
||||||
|
const isConfiguring = computed(() => props.flows?.intro);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -34,15 +34,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RightArrowIcon } from "@modrinth/assets";
|
import { RightArrowIcon } from "@modrinth/assets";
|
||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from "vue-router";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
|
||||||
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
import type { BackupInProgressReason } from "~/pages/servers/manage/[id].vue";
|
||||||
|
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
|
||||||
const emit = defineEmits(["reinstall"]);
|
const emit = defineEmits(["reinstall"]);
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
|
navLinks: { label: string; href: string; icon: Component; external?: boolean }[];
|
||||||
route: RouteLocationNormalized;
|
route: RouteLocationNormalized;
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
data-pyro-server-stats
|
data-pyro-server-stats
|
||||||
style="font-variant-numeric: tabular-nums"
|
style="font-variant-numeric: tabular-nums"
|
||||||
class="flex select-none flex-col items-center gap-6 md:flex-row"
|
class="flex select-none flex-col items-center gap-6 md:flex-row"
|
||||||
|
:class="{ 'pointer-events-none': loading }"
|
||||||
|
:aria-hidden="loading"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(metric, index) in metrics"
|
v-for="(metric, index) in metrics"
|
||||||
@@ -18,7 +20,7 @@
|
|||||||
<h3 class="flex items-center gap-2 text-base font-normal text-secondary">
|
<h3 class="flex items-center gap-2 text-base font-normal text-secondary">
|
||||||
{{ metric.title }}
|
{{ metric.title }}
|
||||||
<IssuesIcon
|
<IssuesIcon
|
||||||
v-if="metric.warning"
|
v-if="metric.warning && !loading"
|
||||||
v-tooltip="metric.warning"
|
v-tooltip="metric.warning"
|
||||||
class="size-5"
|
class="size-5"
|
||||||
:style="{ color: 'var(--color-orange)' }"
|
:style="{ color: 'var(--color-orange)' }"
|
||||||
@@ -28,51 +30,76 @@
|
|||||||
<div class="absolute -left-8 -top-4 h-28 w-56 rounded-full bg-bg-raised blur-lg" />
|
<div class="absolute -left-8 -top-4 h-28 w-56 rounded-full bg-bg-raised blur-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component :is="metric.icon" class="absolute right-10 top-10 z-10" />
|
<component
|
||||||
<ClientOnly>
|
:is="metric.icon"
|
||||||
<VueApexCharts
|
class="absolute right-10 top-10 z-10 size-8"
|
||||||
v-if="metric.showGraph"
|
style="width: 2rem; height: 2rem"
|
||||||
type="area"
|
/>
|
||||||
height="142"
|
|
||||||
:options="getChartOptions(metric.warning)"
|
<div class="chart-space absolute bottom-0 left-0 right-0">
|
||||||
:series="[{ name: metric.title, data: metric.data }]"
|
<ClientOnly>
|
||||||
class="chart absolute bottom-0 left-0 right-0 w-full opacity-0"
|
<VueApexCharts
|
||||||
/>
|
v-if="metric.showGraph && !loading"
|
||||||
</ClientOnly>
|
type="area"
|
||||||
|
height="142"
|
||||||
|
:options="getChartOptions(metric.warning, index)"
|
||||||
|
:series="[{ name: metric.title, data: metric.data }]"
|
||||||
|
class="chart"
|
||||||
|
:class="chartsReady.has(index) ? 'opacity-100' : 'opacity-0'"
|
||||||
|
/>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NuxtLink
|
<component
|
||||||
:to="`/servers/manage/${serverId}/files`"
|
:is="loading ? 'div' : 'NuxtLink'"
|
||||||
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8 transition-transform duration-100 hover:scale-105 active:scale-100"
|
:to="loading ? undefined : `/servers/manage/${serverId}/files`"
|
||||||
|
class="relative isolate min-h-[156px] w-full overflow-hidden rounded-2xl bg-bg-raised p-8"
|
||||||
|
:class="loading ? '' : 'transition-transform duration-100 hover:scale-105 active:scale-100'"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">
|
<h2 class="m-0 -ml-0.5 mt-1 text-3xl font-extrabold text-contrast">
|
||||||
{{ formatBytes(stats.storage_usage_bytes) }}
|
{{ loading ? "0 B" : formatBytes(stats.storage_usage_bytes) }}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
<h3 class="text-base font-normal text-secondary">Storage usage</h3>
|
||||||
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
<FolderOpenIcon class="absolute right-10 top-10 size-8" />
|
||||||
</NuxtLink>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, shallowRef } from "vue";
|
import { ref, computed, shallowRef } from "vue";
|
||||||
import { FolderOpenIcon, CPUIcon, DBIcon, IssuesIcon } from "@modrinth/assets";
|
import { FolderOpenIcon, CPUIcon, DatabaseIcon, IssuesIcon } from "@modrinth/assets";
|
||||||
import { useStorage } from "@vueuse/core";
|
import { useStorage } from "@vueuse/core";
|
||||||
import type { Stats } from "~/types/servers";
|
import type { Stats } from "@modrinth/utils";
|
||||||
|
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
const serverId = route.params.id;
|
const serverId = route.params.id;
|
||||||
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
const VueApexCharts = defineAsyncComponent(() => import("vue3-apexcharts"));
|
||||||
|
|
||||||
|
const chartsReady = ref(new Set<number>());
|
||||||
|
|
||||||
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
const userPreferences = useStorage(`pyro-server-${serverId}-preferences`, {
|
||||||
ramAsNumber: false,
|
ramAsNumber: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{ data: Stats }>();
|
const props = withDefaults(defineProps<{ data?: Stats; loading?: boolean }>(), {
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
const stats = shallowRef(props.data.current);
|
const stats = shallowRef(
|
||||||
|
props.data?.current || {
|
||||||
|
cpu_percent: 0,
|
||||||
|
ram_usage_bytes: 0,
|
||||||
|
ram_total_bytes: 1, // Avoid division by zero
|
||||||
|
storage_usage_bytes: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChartReady = (index: number) => {
|
||||||
|
chartsReady.value.add(index);
|
||||||
|
};
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
const formatBytes = (bytes: number) => {
|
||||||
const units = ["B", "KB", "MB", "GB"];
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
@@ -94,6 +121,29 @@ const updateGraphData = (arr: number[], newValue: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const metrics = computed(() => {
|
const metrics = computed(() => {
|
||||||
|
if (props.loading) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: "CPU usage",
|
||||||
|
value: "0.00%",
|
||||||
|
max: "100%",
|
||||||
|
icon: CPUIcon,
|
||||||
|
data: cpuData.value,
|
||||||
|
showGraph: false,
|
||||||
|
warning: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Memory usage",
|
||||||
|
value: "0.00%",
|
||||||
|
max: "100%",
|
||||||
|
icon: DatabaseIcon,
|
||||||
|
data: ramData.value,
|
||||||
|
showGraph: false,
|
||||||
|
warning: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const ramPercent = Math.min(
|
const ramPercent = Math.min(
|
||||||
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
(stats.value.ram_usage_bytes / stats.value.ram_total_bytes) * 100,
|
||||||
100,
|
100,
|
||||||
@@ -119,7 +169,7 @@ const metrics = computed(() => {
|
|||||||
? formatBytes(stats.value.ram_usage_bytes)
|
? formatBytes(stats.value.ram_usage_bytes)
|
||||||
: `${ramPercent.toFixed(2)}%`,
|
: `${ramPercent.toFixed(2)}%`,
|
||||||
max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%",
|
max: userPreferences.value.ramAsNumber ? formatBytes(stats.value.ram_total_bytes) : "100%",
|
||||||
icon: DBIcon,
|
icon: DatabaseIcon,
|
||||||
data: ramData.value,
|
data: ramData.value,
|
||||||
showGraph: true,
|
showGraph: true,
|
||||||
warning: ramPercent >= 90 ? "Memory usage is very high" : null,
|
warning: ramPercent >= 90 ? "Memory usage is very high" : null,
|
||||||
@@ -127,7 +177,7 @@ const metrics = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const getChartOptions = (hasWarning: string | null) => ({
|
const getChartOptions = (hasWarning: string | null, index: number) => ({
|
||||||
chart: {
|
chart: {
|
||||||
type: "area",
|
type: "area",
|
||||||
animations: { enabled: false },
|
animations: { enabled: false },
|
||||||
@@ -139,6 +189,10 @@ const getChartOptions = (hasWarning: string | null) => ({
|
|||||||
top: 0,
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
|
events: {
|
||||||
|
mounted: () => onChartReady(index),
|
||||||
|
updated: () => onChartReady(index),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
stroke: { curve: "smooth", width: 3 },
|
stroke: { curve: "smooth", width: 3 },
|
||||||
fill: {
|
fill: {
|
||||||
@@ -172,24 +226,26 @@ const getChartOptions = (hasWarning: string | null) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.data.current,
|
() => props.data?.current,
|
||||||
(newStats) => {
|
(newStats) => {
|
||||||
stats.value = newStats;
|
if (newStats) {
|
||||||
|
stats.value = newStats;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart {
|
.chart-space {
|
||||||
animation: fadeIn 0.2s ease-out 0.2s forwards;
|
height: 142px;
|
||||||
|
width: calc(100% + 48px);
|
||||||
margin-left: -24px;
|
margin-left: -24px;
|
||||||
margin-right: -24px;
|
margin-right: -24px;
|
||||||
width: calc(100% + 48px) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.chart {
|
||||||
to {
|
width: 100% !important;
|
||||||
opacity: 1;
|
height: 142px !important;
|
||||||
}
|
transition: opacity 0.3s ease-out;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LoaderIcon } from "@modrinth/assets";
|
import { LoaderIcon } from "@modrinth/assets";
|
||||||
import type { Loaders } from "~/types/servers";
|
import type { Loaders } from "@modrinth/utils";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
loader: Loaders;
|
loader: Loaders;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
||||||
import { RightArrowIcon, SparklesIcon, UnknownIcon } from "@modrinth/assets";
|
|
||||||
import type { MessageDescriptor } from "@vintl/vintl";
|
import type { MessageDescriptor } from "@vintl/vintl";
|
||||||
|
import { formatPrice } from "@modrinth/utils";
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage, locale } = useVIntl();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "select" | "scroll-to-faq"): void;
|
(e: "select" | "scroll-to-faq"): void;
|
||||||
@@ -18,8 +18,8 @@ const plans: Record<
|
|||||||
accentText: string;
|
accentText: string;
|
||||||
accentBg: string;
|
accentBg: string;
|
||||||
name: MessageDescriptor;
|
name: MessageDescriptor;
|
||||||
symbol: MessageDescriptor;
|
|
||||||
description: MessageDescriptor;
|
description: MessageDescriptor;
|
||||||
|
mostPopular: boolean;
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
small: {
|
small: {
|
||||||
@@ -30,15 +30,11 @@ const plans: Record<
|
|||||||
id: "servers.plan.small.name",
|
id: "servers.plan.small.name",
|
||||||
defaultMessage: "Small",
|
defaultMessage: "Small",
|
||||||
}),
|
}),
|
||||||
symbol: defineMessage({
|
|
||||||
id: "servers.plan.small.symbol",
|
|
||||||
defaultMessage: "S",
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.small.description",
|
id: "servers.plan.small.description",
|
||||||
defaultMessage:
|
defaultMessage: "Perfect for 1–5 friends with a few light mods.",
|
||||||
"Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding.",
|
|
||||||
}),
|
}),
|
||||||
|
mostPopular: false,
|
||||||
},
|
},
|
||||||
medium: {
|
medium: {
|
||||||
buttonColor: "green",
|
buttonColor: "green",
|
||||||
@@ -48,14 +44,11 @@ const plans: Record<
|
|||||||
id: "servers.plan.medium.name",
|
id: "servers.plan.medium.name",
|
||||||
defaultMessage: "Medium",
|
defaultMessage: "Medium",
|
||||||
}),
|
}),
|
||||||
symbol: defineMessage({
|
|
||||||
id: "servers.plan.medium.symbol",
|
|
||||||
defaultMessage: "M",
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.medium.description",
|
id: "servers.plan.medium.description",
|
||||||
defaultMessage: "Great for modded multiplayer and small communities.",
|
defaultMessage: "Great for 6–15 players and multiple mods.",
|
||||||
}),
|
}),
|
||||||
|
mostPopular: true,
|
||||||
},
|
},
|
||||||
large: {
|
large: {
|
||||||
buttonColor: "purple",
|
buttonColor: "purple",
|
||||||
@@ -65,14 +58,11 @@ const plans: Record<
|
|||||||
id: "servers.plan.large.name",
|
id: "servers.plan.large.name",
|
||||||
defaultMessage: "Large",
|
defaultMessage: "Large",
|
||||||
}),
|
}),
|
||||||
symbol: defineMessage({
|
|
||||||
id: "servers.plan.large.symbol",
|
|
||||||
defaultMessage: "L",
|
|
||||||
}),
|
|
||||||
description: defineMessage({
|
description: defineMessage({
|
||||||
id: "servers.plan.large.description",
|
id: "servers.plan.large.description",
|
||||||
defaultMessage: "Ideal for larger communities, modpacks, and heavy modding.",
|
defaultMessage: "Ideal for 15–25 players, modpacks, or heavy modding.",
|
||||||
}),
|
}),
|
||||||
|
mostPopular: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,42 +73,30 @@ const props = defineProps<{
|
|||||||
storage: number;
|
storage: number;
|
||||||
cpus: number;
|
cpus: number;
|
||||||
price: number;
|
price: number;
|
||||||
|
interval: "monthly" | "quarterly" | "yearly";
|
||||||
|
currency: string;
|
||||||
|
isUsa: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const outOfStock = computed(() => {
|
const outOfStock = computed(() => {
|
||||||
return !props.capacity || props.capacity === 0;
|
return !props.capacity || props.capacity === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const lowStock = computed(() => {
|
const billingMonths = computed(() => {
|
||||||
return !props.capacity || props.capacity < 8;
|
if (props.interval === "yearly") {
|
||||||
});
|
return 12;
|
||||||
|
} else if (props.interval === "quarterly") {
|
||||||
const formattedRam = computed(() => {
|
return 3;
|
||||||
return props.ram / 1024;
|
}
|
||||||
});
|
return 1;
|
||||||
|
|
||||||
const formattedStorage = computed(() => {
|
|
||||||
return props.storage / 1024;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sharedCpus = computed(() => {
|
|
||||||
return props.cpus / 2;
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="relative flex w-full flex-col justify-between pt-12 lg:w-1/3">
|
<li class="relative flex w-full flex-col justify-between">
|
||||||
<div
|
|
||||||
v-if="lowStock"
|
|
||||||
class="absolute left-0 right-0 top-[-2px] rounded-t-2xl p-4 text-center font-bold"
|
|
||||||
:class="outOfStock ? 'bg-bg-red' : 'bg-bg-orange'"
|
|
||||||
>
|
|
||||||
<template v-if="outOfStock"> Out of stock! </template>
|
|
||||||
<template v-else> Only {{ capacity }} left in stock! </template>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
:style="
|
:style="
|
||||||
plan === 'medium'
|
plans[plan].mostPopular
|
||||||
? {
|
? {
|
||||||
background: `radial-gradient(
|
background: `radial-gradient(
|
||||||
86.12% 101.64% at 95.97% 94.07%,
|
86.12% 101.64% at 95.97% 94.07%,
|
||||||
@@ -131,55 +109,41 @@ const sharedCpus = computed(() => {
|
|||||||
: undefined
|
: undefined
|
||||||
"
|
"
|
||||||
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left"
|
class="flex w-full flex-col justify-between gap-4 rounded-2xl bg-bg p-8 text-left"
|
||||||
:class="{ '!rounded-t-none': lowStock }"
|
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row items-center justify-between">
|
<div class="flex flex-row flex-wrap items-center gap-3">
|
||||||
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
|
<h1 class="m-0">{{ formatMessage(plans[plan].name) }}</h1>
|
||||||
<div
|
<div
|
||||||
class="grid size-8 place-content-center rounded-full text-xs font-bold"
|
v-if="plans[plan].mostPopular"
|
||||||
:class="`${plans[plan].accentBg} ${plans[plan].accentText}`"
|
class="rounded-full bg-brand-highlight px-2 py-1 text-xs font-bold text-brand"
|
||||||
>
|
>
|
||||||
{{ formatMessage(plans[plan].symbol) }}
|
Most popular
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="m-0">{{ formatMessage(plans[plan].description) }}</p>
|
|
||||||
<div
|
|
||||||
class="flex flex-row flex-wrap items-center gap-2 text-nowrap text-secondary xl:justify-between"
|
|
||||||
>
|
|
||||||
<p class="m-0">{{ formattedRam }} GB RAM</p>
|
|
||||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
|
||||||
<p class="m-0">{{ formattedStorage }} GB SSD</p>
|
|
||||||
<div class="size-1.5 rounded-full bg-secondary opacity-25"></div>
|
|
||||||
<p class="m-0">{{ sharedCpus }} Shared CPUs</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-secondary">
|
|
||||||
<SparklesIcon /> Bursts up to {{ cpus }} CPUs
|
|
||||||
<nuxt-link
|
|
||||||
v-tooltip="
|
|
||||||
`CPU bursting allows your server to temporarily use additional threads to help mitigate TPS spikes. Click for more info.`
|
|
||||||
"
|
|
||||||
to="/servers#cpu-burst"
|
|
||||||
@click="() => emit('scroll-to-faq')"
|
|
||||||
>
|
|
||||||
<UnknownIcon class="h-4 w-4 text-secondary opacity-80" />
|
|
||||||
</nuxt-link>
|
|
||||||
</div>
|
|
||||||
<span class="m-0 text-2xl font-bold text-contrast">
|
<span class="m-0 text-2xl font-bold text-contrast">
|
||||||
${{ price / 100 }}<span class="text-lg font-semibold text-secondary">/month</span>
|
{{ formatPrice(locale, price / billingMonths, currency, true) }}
|
||||||
|
{{ isUsa ? "" : currency }}
|
||||||
|
<span class="text-lg font-semibold text-secondary">
|
||||||
|
/ month<template v-if="interval !== 'monthly'">, billed {{ interval }}</template>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<p class="m-0 max-w-[18rem]">{{ formatMessage(plans[plan].description) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
:color="plans[plan].buttonColor"
|
:color="plans[plan].buttonColor"
|
||||||
:type="plan === 'medium' ? 'standard' : 'highlight-colored-text'"
|
:type="plans[plan].mostPopular ? 'standard' : 'highlight-colored-text'"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<span v-if="outOfStock" class="button-like disabled"> Out of Stock </span>
|
<span v-if="outOfStock" class="button-like disabled"> Out of Stock </span>
|
||||||
<button v-else @click="() => emit('select')">
|
<button v-else @click="() => emit('select')">Select plan</button>
|
||||||
Get Started
|
|
||||||
<RightArrowIcon class="shrink-0" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
<ServersSpecs
|
||||||
|
:ram="ram"
|
||||||
|
:storage="storage"
|
||||||
|
:cpus="cpus"
|
||||||
|
:bursting-link="'/servers#cpu-burst'"
|
||||||
|
@click-bursting-link="() => emit('scroll-to-faq')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Accordion, ButtonStyled, NewModal, ServerNotice, TagItem } from "@modri
|
|||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
|
|
||||||
const app = useNuxtApp() as unknown as { $notify: any };
|
const app = useNuxtApp() as unknown as { $notify: any };
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ const assignedNodes = computed(() => assigned.value.filter((n) => n.kind === "no
|
|||||||
const inputField = ref("");
|
const inputField = ref("");
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
await usePyroFetch("notices").then((res) => {
|
await useServersFetch("notices").then((res) => {
|
||||||
const notices = res as ServerNoticeType[];
|
const notices = res as ServerNoticeType[];
|
||||||
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
|
assigned.value = notices.find((n) => n.id === notice.value?.id)?.assigned ?? [];
|
||||||
});
|
});
|
||||||
@@ -33,9 +33,12 @@ async function assign(server: boolean = true) {
|
|||||||
const input = inputField.value.trim();
|
const input = inputField.value.trim();
|
||||||
|
|
||||||
if (input !== "" && notice.value) {
|
if (input !== "" && notice.value) {
|
||||||
await usePyroFetch(`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`, {
|
await useServersFetch(
|
||||||
method: "PUT",
|
`notices/${notice.value.id}/assign?${server ? "server" : "node"}=${input}`,
|
||||||
}).catch((err) => {
|
{
|
||||||
|
method: "PUT",
|
||||||
|
},
|
||||||
|
).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: "main",
|
||||||
title: "Error assigning notice",
|
title: "Error assigning notice",
|
||||||
@@ -75,9 +78,12 @@ async function unassignDetect() {
|
|||||||
|
|
||||||
async function unassign(id: string, server: boolean = true) {
|
async function unassign(id: string, server: boolean = true) {
|
||||||
if (notice.value) {
|
if (notice.value) {
|
||||||
await usePyroFetch(`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`, {
|
await useServersFetch(
|
||||||
method: "PUT",
|
`notices/${notice.value.id}/unassign?${server ? "server" : "node"}=${id}`,
|
||||||
}).catch((err) => {
|
{
|
||||||
|
method: "PUT",
|
||||||
|
},
|
||||||
|
).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
group: "main",
|
group: "main",
|
||||||
title: "Error unassigning notice",
|
title: "Error unassigning notice",
|
||||||
|
|||||||
@@ -2,13 +2,8 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
|
import { ButtonStyled, commonMessages, CopyCode, ServerNotice, TagItem } from "@modrinth/ui";
|
||||||
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
|
import { EditIcon, SettingsIcon, TrashIcon } from "@modrinth/assets";
|
||||||
import { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||||
import { useRelativeTime } from "@modrinth/ui";
|
import { useRelativeTime, getDismissableMetadata, NOTICE_LEVELS } from "@modrinth/ui";
|
||||||
import {
|
|
||||||
DISMISSABLE,
|
|
||||||
getDismissableMetadata,
|
|
||||||
NOTICE_LEVELS,
|
|
||||||
} from "@modrinth/ui/src/utils/notices.ts";
|
|
||||||
import { useVIntl } from "@vintl/vintl";
|
import { useVIntl } from "@vintl/vintl";
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { OverflowMenu, MarkdownEditor } from "@modrinth/ui";
|
import { CopyCode, OverflowMenu, MarkdownEditor } from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
ReplyIcon,
|
ReplyIcon,
|
||||||
@@ -226,7 +226,6 @@ import {
|
|||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { useImageUpload } from "~/composables/image-upload.ts";
|
import { useImageUpload } from "~/composables/image-upload.ts";
|
||||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
|
||||||
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
import ThreadMessage from "~/components/ui/thread/ThreadMessage.vue";
|
||||||
import { isStaff } from "~/helpers/users.js";
|
import { isStaff } from "~/helpers/users.js";
|
||||||
import { isApproved, isRejected } from "~/helpers/projects.js";
|
import { isApproved, isRejected } from "~/helpers/projects.js";
|
||||||
|
|||||||
@@ -103,10 +103,8 @@ import {
|
|||||||
ModrinthIcon,
|
ModrinthIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { AutoLink, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
import { AutoLink, Avatar, Badge, OverflowMenu, useRelativeTime } from "@modrinth/ui";
|
||||||
import { renderString } from "@modrinth/utils";
|
import { renderString } from "@modrinth/utils";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import { isStaff } from "~/helpers/users.js";
|
import { isStaff } from "~/helpers/users.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const useUserCountry = () => {
|
|||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (fromServer.value) return;
|
if (fromServer.value) return;
|
||||||
|
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
|
||||||
const lang = navigator.language || navigator.userLanguage || "";
|
const lang = navigator.language || navigator.userLanguage || "";
|
||||||
const region = lang.split("-")[1];
|
const region = lang.split("-")[1];
|
||||||
if (region) {
|
if (region) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
|
|||||||
|
|
||||||
// Make sure file is less than 1MB
|
// Make sure file is less than 1MB
|
||||||
if (file.size > 1024 * 1024) {
|
if (file.size > 1024 * 1024) {
|
||||||
throw new Error("File is too large");
|
throw new Error("File exceeds the 1MiB size limit");
|
||||||
}
|
}
|
||||||
|
|
||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import { $fetch, FetchError } from "ofetch";
|
|
||||||
|
|
||||||
interface PyroFetchOptions {
|
|
||||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
||||||
contentType?: string;
|
|
||||||
body?: Record<string, any>;
|
|
||||||
version?: number;
|
|
||||||
override?: {
|
|
||||||
url?: string;
|
|
||||||
token?: string;
|
|
||||||
};
|
|
||||||
retry?: boolean;
|
|
||||||
bypassAuth?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PyroFetchError extends Error {
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
public statusCode?: number,
|
|
||||||
public originalError?: Error,
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = "PyroFetchError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function usePyroFetch<T>(path: string, options: PyroFetchOptions = {}): Promise<T> {
|
|
||||||
const config = useRuntimeConfig();
|
|
||||||
const auth = await useAuth();
|
|
||||||
const authToken = auth.value?.token;
|
|
||||||
|
|
||||||
if (!authToken && !options.bypassAuth) {
|
|
||||||
throw new PyroFetchError("Cannot pyrofetch without auth", 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { method = "GET", contentType = "application/json", body, version = 0, override } = options;
|
|
||||||
|
|
||||||
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
|
||||||
/\/$/,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!base) {
|
|
||||||
throw new PyroFetchError(
|
|
||||||
"Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
|
|
||||||
10001,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullUrl = override?.url
|
|
||||||
? `https://${override.url}/${path.replace(/^\//, "")}`
|
|
||||||
: `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`;
|
|
||||||
|
|
||||||
type HeadersRecord = Record<string, string>;
|
|
||||||
|
|
||||||
const authHeader: HeadersRecord = options.bypassAuth
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
Authorization: `Bearer ${override?.token ?? authToken}`,
|
|
||||||
"Access-Control-Allow-Headers": "Authorization",
|
|
||||||
};
|
|
||||||
|
|
||||||
const headers: HeadersRecord = {
|
|
||||||
...authHeader,
|
|
||||||
"User-Agent": "Pyro/1.0 (https://pyro.host)",
|
|
||||||
Vary: "Accept, Origin",
|
|
||||||
"Content-Type": contentType,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (import.meta.client && typeof window !== "undefined") {
|
|
||||||
headers.Origin = window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await $fetch<T>(fullUrl, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
|
||||||
timeout: 10000,
|
|
||||||
retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0,
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Fetch error:", error);
|
|
||||||
if (error instanceof FetchError) {
|
|
||||||
const statusCode = error.response?.status;
|
|
||||||
const statusText = error.response?.statusText || "Unknown error";
|
|
||||||
const errorMessages: { [key: number]: string } = {
|
|
||||||
400: "Bad Request",
|
|
||||||
401: "Unauthorized",
|
|
||||||
403: "Forbidden",
|
|
||||||
404: "Not Found",
|
|
||||||
405: "Method Not Allowed",
|
|
||||||
429: "Too Many Requests",
|
|
||||||
500: "Internal Server Error",
|
|
||||||
502: "Bad Gateway",
|
|
||||||
};
|
|
||||||
const message =
|
|
||||||
statusCode && statusCode in errorMessages
|
|
||||||
? errorMessages[statusCode]
|
|
||||||
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
|
||||||
throw new PyroFetchError(`[PYROFETCH][PYRO] ${message}`, statusCode, error);
|
|
||||||
}
|
|
||||||
throw new PyroFetchError(
|
|
||||||
"[PYROFETCH][PYRO] An unexpected error occurred during the fetch operation.",
|
|
||||||
undefined,
|
|
||||||
error as Error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
282
apps/frontend/src/composables/servers/modrinth-servers.ts
Normal file
282
apps/frontend/src/composables/servers/modrinth-servers.ts
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import { ModrinthServerError } from "@modrinth/utils";
|
||||||
|
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
|
||||||
|
import { useServersFetch } from "./servers-fetch.ts";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GeneralModule,
|
||||||
|
ContentModule,
|
||||||
|
BackupsModule,
|
||||||
|
NetworkModule,
|
||||||
|
StartupModule,
|
||||||
|
WSModule,
|
||||||
|
FSModule,
|
||||||
|
} from "./modules/index.ts";
|
||||||
|
|
||||||
|
export function handleError(err: any) {
|
||||||
|
if (err instanceof ModrinthServerError && err.v1Error) {
|
||||||
|
addNotification({
|
||||||
|
title: err.v1Error?.context ?? `An error occurred`,
|
||||||
|
type: "error",
|
||||||
|
text: err.v1Error.description,
|
||||||
|
errorCode: err.v1Error.error,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addNotification({
|
||||||
|
title: "An error occurred",
|
||||||
|
type: "error",
|
||||||
|
text: err.message ?? (err.data ? err.data.description : err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModrinthServer {
|
||||||
|
readonly serverId: string;
|
||||||
|
private errors: Partial<Record<ModuleName, ModuleError>> = {};
|
||||||
|
|
||||||
|
readonly general: GeneralModule;
|
||||||
|
readonly content: ContentModule;
|
||||||
|
readonly backups: BackupsModule;
|
||||||
|
readonly network: NetworkModule;
|
||||||
|
readonly startup: StartupModule;
|
||||||
|
readonly ws: WSModule;
|
||||||
|
readonly fs: FSModule;
|
||||||
|
|
||||||
|
constructor(serverId: string) {
|
||||||
|
this.serverId = serverId;
|
||||||
|
|
||||||
|
this.general = new GeneralModule(this);
|
||||||
|
this.content = new ContentModule(this);
|
||||||
|
this.backups = new BackupsModule(this);
|
||||||
|
this.network = new NetworkModule(this);
|
||||||
|
this.startup = new StartupModule(this);
|
||||||
|
this.ws = new WSModule(this);
|
||||||
|
this.fs = new FSModule(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMissingFolders(path: string): Promise<void> {
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
path = path.substring(1);
|
||||||
|
}
|
||||||
|
const folders = path.split("/");
|
||||||
|
let currentPath = "";
|
||||||
|
|
||||||
|
for (const folder of folders) {
|
||||||
|
currentPath += "/" + folder;
|
||||||
|
try {
|
||||||
|
await this.fs.createFileOrFolder(currentPath, "directory");
|
||||||
|
} catch {
|
||||||
|
// Folder might already exist, ignore error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchConfigFile(fileName: string): Promise<any> {
|
||||||
|
return await useServersFetch(`servers/${this.serverId}/config/${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructServerProperties(properties: any): string {
|
||||||
|
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
|
if (typeof value === "object") {
|
||||||
|
fileContent += `${key}=${JSON.stringify(value)}\n`;
|
||||||
|
} else if (typeof value === "boolean") {
|
||||||
|
fileContent += `${key}=${value ? "true" : "false"}\n`;
|
||||||
|
} else {
|
||||||
|
fileContent += `${key}=${value}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
|
||||||
|
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`);
|
||||||
|
|
||||||
|
if (sharedImage.value) {
|
||||||
|
return sharedImage.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
||||||
|
try {
|
||||||
|
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
|
||||||
|
override: auth,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fileData instanceof Blob && import.meta.client) {
|
||||||
|
const dataURL = await new Promise<string>((resolve) => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
canvas.width = 512;
|
||||||
|
canvas.height = 512;
|
||||||
|
ctx?.drawImage(img, 0, 0, 512, 512);
|
||||||
|
const dataURL = canvas.toDataURL("image/png");
|
||||||
|
sharedImage.value = dataURL;
|
||||||
|
resolve(dataURL);
|
||||||
|
URL.revokeObjectURL(img.src);
|
||||||
|
};
|
||||||
|
img.src = URL.createObjectURL(fileData);
|
||||||
|
});
|
||||||
|
return dataURL;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ModrinthServerError && error.statusCode === 404) {
|
||||||
|
if (iconUrl) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(iconUrl);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch icon");
|
||||||
|
const file = await response.blob();
|
||||||
|
const originalFile = new File([file], "server-icon-original.png", {
|
||||||
|
type: "image/png",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
const dataURL = await new Promise<string>((resolve) => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
canvas.width = 64;
|
||||||
|
canvas.height = 64;
|
||||||
|
ctx?.drawImage(img, 0, 0, 64, 64);
|
||||||
|
canvas.toBlob(async (blob) => {
|
||||||
|
if (blob) {
|
||||||
|
const scaledFile = new File([blob], "server-icon.png", {
|
||||||
|
type: "image/png",
|
||||||
|
});
|
||||||
|
await useServersFetch(`/create?path=/server-icon.png&type=file`, {
|
||||||
|
method: "POST",
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
body: scaledFile,
|
||||||
|
override: auth,
|
||||||
|
});
|
||||||
|
await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
|
||||||
|
method: "POST",
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
body: originalFile,
|
||||||
|
override: auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, "image/png");
|
||||||
|
const dataURL = canvas.toDataURL("image/png");
|
||||||
|
sharedImage.value = dataURL;
|
||||||
|
resolve(dataURL);
|
||||||
|
URL.revokeObjectURL(img.src);
|
||||||
|
};
|
||||||
|
img.src = URL.createObjectURL(file);
|
||||||
|
});
|
||||||
|
return dataURL;
|
||||||
|
}
|
||||||
|
} catch (externalError: any) {
|
||||||
|
console.debug("Could not process external icon:", externalError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.debug("Icon processing failed:", error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedImage.value = undefined;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(
|
||||||
|
modules: ModuleName[] = [],
|
||||||
|
options?: {
|
||||||
|
preserveConnection?: boolean;
|
||||||
|
preserveInstallState?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const modulesToRefresh =
|
||||||
|
modules.length > 0
|
||||||
|
? modules
|
||||||
|
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
|
||||||
|
|
||||||
|
for (const module of modulesToRefresh) {
|
||||||
|
try {
|
||||||
|
switch (module) {
|
||||||
|
case "general": {
|
||||||
|
if (options?.preserveConnection) {
|
||||||
|
const currentImage = this.general.image;
|
||||||
|
const currentMotd = this.general.motd;
|
||||||
|
const currentStatus = this.general.status;
|
||||||
|
|
||||||
|
await this.general.fetch();
|
||||||
|
|
||||||
|
if (currentImage) {
|
||||||
|
this.general.image = currentImage;
|
||||||
|
}
|
||||||
|
if (currentMotd) {
|
||||||
|
this.general.motd = currentMotd;
|
||||||
|
}
|
||||||
|
if (options.preserveInstallState && currentStatus === "installing") {
|
||||||
|
this.general.status = "installing";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.general.fetch();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "content":
|
||||||
|
await this.content.fetch();
|
||||||
|
break;
|
||||||
|
case "backups":
|
||||||
|
await this.backups.fetch();
|
||||||
|
break;
|
||||||
|
case "network":
|
||||||
|
await this.network.fetch();
|
||||||
|
break;
|
||||||
|
case "startup":
|
||||||
|
await this.startup.fetch();
|
||||||
|
break;
|
||||||
|
case "ws":
|
||||||
|
await this.ws.fetch();
|
||||||
|
break;
|
||||||
|
case "fs":
|
||||||
|
await this.fs.fetch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ModrinthServerError) {
|
||||||
|
if (error.statusCode === 404 && ["fs", "content"].includes(module)) {
|
||||||
|
console.debug(`Optional ${module} resource not found:`, error.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode === 503) {
|
||||||
|
console.debug(`Temporary ${module} unavailable:`, error.message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.errors[module] = {
|
||||||
|
error:
|
||||||
|
error instanceof ModrinthServerError
|
||||||
|
? error
|
||||||
|
: new ModrinthServerError("Unknown error", undefined, error as Error),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get moduleErrors() {
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModrinthServers = async (
|
||||||
|
serverId: string,
|
||||||
|
includedModules: ModuleName[] = ["general"],
|
||||||
|
) => {
|
||||||
|
const server = new ModrinthServer(serverId);
|
||||||
|
await server.refresh(includedModules);
|
||||||
|
return reactive(server);
|
||||||
|
};
|
||||||
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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
179
apps/frontend/src/composables/servers/modules/general.ts
Normal file
179
apps/frontend/src/composables/servers/modules/general.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { $fetch } from "ofetch";
|
||||||
|
import type { ServerGeneral, Project, PowerAction, JWTAuth } from "@modrinth/utils";
|
||||||
|
import { useServersFetch } from "../servers-fetch.ts";
|
||||||
|
import { ServerModule } from "./base.ts";
|
||||||
|
|
||||||
|
export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||||
|
server_id!: string;
|
||||||
|
name!: string;
|
||||||
|
net!: { ip: string; port: number; domain: string };
|
||||||
|
game!: string;
|
||||||
|
backup_quota!: number;
|
||||||
|
used_backup_quota!: number;
|
||||||
|
status!: string;
|
||||||
|
suspension_reason!: string;
|
||||||
|
loader!: string;
|
||||||
|
loader_version!: string;
|
||||||
|
mc_version!: string;
|
||||||
|
upstream!: {
|
||||||
|
kind: "modpack" | "mod" | "resourcepack";
|
||||||
|
version_id: string;
|
||||||
|
project_id: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
motd?: string;
|
||||||
|
image?: string;
|
||||||
|
project?: Project;
|
||||||
|
sftp_username!: string;
|
||||||
|
sftp_password!: string;
|
||||||
|
sftp_host!: string;
|
||||||
|
datacenter?: string;
|
||||||
|
notices?: any[];
|
||||||
|
node!: { token: string; instance: string };
|
||||||
|
flows?: { intro?: boolean };
|
||||||
|
|
||||||
|
async fetch(): Promise<void> {
|
||||||
|
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, "general");
|
||||||
|
|
||||||
|
if (data.upstream?.project_id) {
|
||||||
|
const project = await $fetch(
|
||||||
|
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
|
||||||
|
);
|
||||||
|
data.project = project as Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const motd = await this.getMotd();
|
||||||
|
if (motd === "A Minecraft Server") {
|
||||||
|
await this.setMotd(
|
||||||
|
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
data.motd = motd;
|
||||||
|
|
||||||
|
// Copy data to this module
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateName(newName: string): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/name`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { name: newName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async power(action: PowerAction): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/power`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { action },
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
await this.fetch(); // Refresh this module
|
||||||
|
}
|
||||||
|
|
||||||
|
async reinstall(
|
||||||
|
loader: boolean,
|
||||||
|
projectId: string,
|
||||||
|
versionId?: string,
|
||||||
|
loaderVersionId?: string,
|
||||||
|
hardReset: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
const hardResetParam = hardReset ? "true" : "false";
|
||||||
|
if (loader) {
|
||||||
|
if (projectId.toLowerCase() === "neoforge") {
|
||||||
|
projectId = "NeoForge";
|
||||||
|
}
|
||||||
|
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { loader: projectId, loader_version: loaderVersionId, game_version: versionId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { project_id: projectId, version_id: versionId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reinstallFromMrpack(mrpack: File, hardReset: boolean = false): Promise<void> {
|
||||||
|
const hardResetParam = hardReset ? "true" : "false";
|
||||||
|
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", mrpack);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${auth.token}`,
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
signal: AbortSignal.timeout(30 * 60 * 1000),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`[pyroservers] native fetch err status: ${response.status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async suspend(status: boolean): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/suspend`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { suspended: status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async endIntro(): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/flows/intro`, {
|
||||||
|
method: "DELETE",
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
await this.fetch(); // Refresh this module
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMotd(): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const props = await this.server.fs.downloadFile("/server.properties");
|
||||||
|
if (props) {
|
||||||
|
const lines = props.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith("motd=")) {
|
||||||
|
return line.slice(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMotd(motd: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
|
||||||
|
if (props) {
|
||||||
|
props.motd = motd;
|
||||||
|
const newProps = this.server.constructServerProperties(props);
|
||||||
|
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
|
||||||
|
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
|
||||||
|
|
||||||
|
await useServersFetch(`/update?path=/server.properties`, {
|
||||||
|
method: "PUT",
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
body: octetStream,
|
||||||
|
override: auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error(
|
||||||
|
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
apps/frontend/src/composables/servers/modules/index.ts
Normal file
8
apps/frontend/src/composables/servers/modules/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export * from "./base.ts";
|
||||||
|
export * from "./backups.ts";
|
||||||
|
export * from "./content.ts";
|
||||||
|
export * from "./fs.ts";
|
||||||
|
export * from "./general.ts";
|
||||||
|
export * from "./network.ts";
|
||||||
|
export * from "./startup.ts";
|
||||||
|
export * from "./ws.ts";
|
||||||
47
apps/frontend/src/composables/servers/modules/network.ts
Normal file
47
apps/frontend/src/composables/servers/modules/network.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Allocation } from "@modrinth/utils";
|
||||||
|
import { useServersFetch } from "../servers-fetch.ts";
|
||||||
|
import { ServerModule } from "./base.ts";
|
||||||
|
|
||||||
|
export class NetworkModule extends ServerModule {
|
||||||
|
allocations: Allocation[] = [];
|
||||||
|
|
||||||
|
async fetch(): Promise<void> {
|
||||||
|
this.allocations = await useServersFetch<Allocation[]>(
|
||||||
|
`servers/${this.serverId}/allocations`,
|
||||||
|
{},
|
||||||
|
"network",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async reserveAllocation(name: string): Promise<Allocation> {
|
||||||
|
return await useServersFetch<Allocation>(`servers/${this.serverId}/allocations?name=${name}`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAllocation(port: number, name: string): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/allocations/${port}?name=${name}`, {
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllocation(port: number): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/allocations/${port}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkSubdomainAvailability(subdomain: string): Promise<boolean> {
|
||||||
|
const result = (await useServersFetch(`subdomains/${subdomain}/isavailable`)) as {
|
||||||
|
available: boolean;
|
||||||
|
};
|
||||||
|
return result.available;
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeSubdomain(subdomain: string): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/subdomain`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { subdomain },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
26
apps/frontend/src/composables/servers/modules/startup.ts
Normal file
26
apps/frontend/src/composables/servers/modules/startup.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Startup, JDKVersion, JDKBuild } from "@modrinth/utils";
|
||||||
|
import { useServersFetch } from "../servers-fetch.ts";
|
||||||
|
import { ServerModule } from "./base.ts";
|
||||||
|
|
||||||
|
export class StartupModule extends ServerModule implements Startup {
|
||||||
|
invocation!: string;
|
||||||
|
original_invocation!: string;
|
||||||
|
jdk_version!: JDKVersion;
|
||||||
|
jdk_build!: JDKBuild;
|
||||||
|
|
||||||
|
async fetch(): Promise<void> {
|
||||||
|
const data = await useServersFetch<Startup>(`servers/${this.serverId}/startup`, {}, "startup");
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(invocation: string, jdkVersion: JDKVersion, jdkBuild: JDKBuild): Promise<void> {
|
||||||
|
await useServersFetch(`servers/${this.serverId}/startup`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
invocation: invocation || null,
|
||||||
|
jdk_version: jdkVersion || null,
|
||||||
|
jdk_build: jdkBuild || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/frontend/src/composables/servers/modules/ws.ts
Normal file
13
apps/frontend/src/composables/servers/modules/ws.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import type { JWTAuth } from "@modrinth/utils";
|
||||||
|
import { useServersFetch } from "../servers-fetch.ts";
|
||||||
|
import { ServerModule } from "./base.ts";
|
||||||
|
|
||||||
|
export class WSModule extends ServerModule implements JWTAuth {
|
||||||
|
url!: string;
|
||||||
|
token!: string;
|
||||||
|
|
||||||
|
async fetch(): Promise<void> {
|
||||||
|
const data = await useServersFetch<JWTAuth>(`servers/${this.serverId}/ws`, {}, "ws");
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
apps/frontend/src/composables/servers/servers-fetch.ts
Normal file
188
apps/frontend/src/composables/servers/servers-fetch.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { $fetch, FetchError } from "ofetch";
|
||||||
|
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
|
import type { V1ErrorInfo } from "@modrinth/utils";
|
||||||
|
|
||||||
|
export interface ServersFetchOptions {
|
||||||
|
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
contentType?: string;
|
||||||
|
body?: Record<string, any>;
|
||||||
|
version?: number;
|
||||||
|
override?: {
|
||||||
|
url?: string;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
retry?: number | boolean;
|
||||||
|
bypassAuth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function useServersFetch<T>(
|
||||||
|
path: string,
|
||||||
|
options: ServersFetchOptions = {},
|
||||||
|
module?: string,
|
||||||
|
errorContext?: string,
|
||||||
|
): Promise<T> {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const auth = await useAuth();
|
||||||
|
const authToken = auth.value?.token;
|
||||||
|
|
||||||
|
if (!authToken && !options.bypassAuth) {
|
||||||
|
const error = new ModrinthServersFetchError(
|
||||||
|
"[Modrinth Servers] Cannot fetch without auth",
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Missing auth token", 401, error, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
method = "GET",
|
||||||
|
contentType = "application/json",
|
||||||
|
body,
|
||||||
|
version = 0,
|
||||||
|
override,
|
||||||
|
retry = method === "GET" ? 3 : 0,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
||||||
|
/\/$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!base) {
|
||||||
|
const error = new ModrinthServersFetchError(
|
||||||
|
"[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
|
||||||
|
10001,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Configuration error: Missing PYRO_BASE_URL", 500, error, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionString = `v${version}`;
|
||||||
|
let newOverrideUrl = override?.url;
|
||||||
|
if (newOverrideUrl && newOverrideUrl.includes("v0") && version !== 0) {
|
||||||
|
newOverrideUrl = newOverrideUrl.replace("v0", versionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = newOverrideUrl
|
||||||
|
? `https://${newOverrideUrl}/${path.replace(/^\//, "")}`
|
||||||
|
: version === 0
|
||||||
|
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
|
||||||
|
: `${base}/v${version}/${path.replace(/^\//, "")}`;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
||||||
|
Vary: "Accept, Origin",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.bypassAuth) {
|
||||||
|
headers.Authorization = `Bearer ${override?.token ?? authToken}`;
|
||||||
|
headers["Access-Control-Allow-Headers"] = "Authorization";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType !== "none") {
|
||||||
|
headers["Content-Type"] = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client && typeof window !== "undefined") {
|
||||||
|
headers.Origin = window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = (typeof retry === "boolean" ? (retry ? 3 : 1) : retry) + 1;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
try {
|
||||||
|
const response = await $fetch<T>(fullUrl, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (error instanceof FetchError) {
|
||||||
|
const statusCode = error.response?.status;
|
||||||
|
const statusText = error.response?.statusText || "Unknown error";
|
||||||
|
|
||||||
|
let v1Error: V1ErrorInfo | undefined;
|
||||||
|
if (error.data?.error && error.data?.description) {
|
||||||
|
v1Error = {
|
||||||
|
context: errorContext,
|
||||||
|
...error.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessages: { [key: number]: string } = {
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Not Found",
|
||||||
|
405: "Method Not Allowed",
|
||||||
|
408: "Request Timeout",
|
||||||
|
429: "Too Many Requests",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
502: "Bad Gateway",
|
||||||
|
503: "Service Unavailable",
|
||||||
|
504: "Gateway Timeout",
|
||||||
|
};
|
||||||
|
|
||||||
|
const message =
|
||||||
|
statusCode && statusCode in errorMessages
|
||||||
|
? errorMessages[statusCode]
|
||||||
|
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
||||||
|
|
||||||
|
const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true;
|
||||||
|
|
||||||
|
if (!isRetryable || attempts >= maxAttempts) {
|
||||||
|
console.error("Fetch error:", error);
|
||||||
|
|
||||||
|
const fetchError = new ModrinthServersFetchError(
|
||||||
|
`[Modrinth Servers] ${message}`,
|
||||||
|
statusCode,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
|
||||||
|
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Unexpected fetch error:", error);
|
||||||
|
const fetchError = new ModrinthServersFetchError(
|
||||||
|
"[Modrinth Servers] An unexpected error occurred during the fetch operation.",
|
||||||
|
undefined,
|
||||||
|
error as Error,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError(
|
||||||
|
"Unexpected error during fetch operation",
|
||||||
|
undefined,
|
||||||
|
fetchError,
|
||||||
|
module,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("All retry attempts failed:", lastError);
|
||||||
|
if (lastError instanceof FetchError) {
|
||||||
|
const statusCode = lastError.response?.status;
|
||||||
|
const pyroError = new ModrinthServersFetchError(
|
||||||
|
"Maximum retry attempts reached",
|
||||||
|
statusCode,
|
||||||
|
lastError,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Maximum retry attempts reached", statusCode, pyroError, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchError = new ModrinthServersFetchError(
|
||||||
|
"Maximum retry attempts reached",
|
||||||
|
undefined,
|
||||||
|
lastError || undefined,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Maximum retry attempts reached", undefined, fetchError, module);
|
||||||
|
}
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
"message": "Subscribe to updates about Modrinth"
|
"message": "Subscribe to updates about Modrinth"
|
||||||
},
|
},
|
||||||
"auth.welcome.description": {
|
"auth.welcome.description": {
|
||||||
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with amazing mods."
|
"message": "You’re now part of the awesome community of creators & explorers already building, downloading, and staying up-to-date with awazing mods."
|
||||||
},
|
},
|
||||||
"auth.welcome.label.tos": {
|
"auth.welcome.label.tos": {
|
||||||
"message": "By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
"message": "By creating an account, you have agreed to Modrinth's <terms-link>Terms</terms-link> and <privacy-policy-link>Privacy Policy</privacy-policy-link>."
|
||||||
@@ -350,11 +350,14 @@
|
|||||||
"layout.banner.add-email.button": {
|
"layout.banner.add-email.button": {
|
||||||
"message": "Visit account settings"
|
"message": "Visit account settings"
|
||||||
},
|
},
|
||||||
|
"layout.banner.add-email.description": {
|
||||||
|
"message": "For security reasons, Modrinth needs you to register an email address to your account."
|
||||||
|
},
|
||||||
"layout.banner.build-fail.description": {
|
"layout.banner.build-fail.description": {
|
||||||
"message": "This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}"
|
"message": "This deploy of Modrinth's frontend failed to generate state from the API. This may be due to an outage or an error in configuration. Rebuild when the API is available. Error codes: {errors}; Current API URL is: {url}"
|
||||||
},
|
},
|
||||||
"layout.banner.build-fail.title": {
|
"layout.banner.build-fail.title": {
|
||||||
"message": "Error generating state from API when building"
|
"message": "Error generating state from API when building."
|
||||||
},
|
},
|
||||||
"layout.banner.staging.description": {
|
"layout.banner.staging.description": {
|
||||||
"message": "The staging environment is completely separate from the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance."
|
"message": "The staging environment is completely separate from the production Modrinth database. This is used for testing and debugging purposes, and may be running in-development versions of the Modrinth backend or frontend newer than the production instance."
|
||||||
@@ -365,12 +368,12 @@
|
|||||||
"layout.banner.subscription-payment-failed.button": {
|
"layout.banner.subscription-payment-failed.button": {
|
||||||
"message": "Update billing info"
|
"message": "Update billing info"
|
||||||
},
|
},
|
||||||
"layout.banner.subscription-payment-failed.title": {
|
|
||||||
"message": "Billing action required"
|
|
||||||
},
|
|
||||||
"layout.banner.subscription-payment-failed.description": {
|
"layout.banner.subscription-payment-failed.description": {
|
||||||
"message": "One or more subscriptions failed to renew. Please update your payment method to prevent losing access!"
|
"message": "One or more subscriptions failed to renew. Please update your payment method to prevent losing access!"
|
||||||
},
|
},
|
||||||
|
"layout.banner.subscription-payment-failed.title": {
|
||||||
|
"message": "Billing action required."
|
||||||
|
},
|
||||||
"layout.banner.verify-email.action": {
|
"layout.banner.verify-email.action": {
|
||||||
"message": "Re-send verification email"
|
"message": "Re-send verification email"
|
||||||
},
|
},
|
||||||
@@ -1047,32 +1050,23 @@
|
|||||||
"message": "No notices"
|
"message": "No notices"
|
||||||
},
|
},
|
||||||
"servers.plan.large.description": {
|
"servers.plan.large.description": {
|
||||||
"message": "Ideal for larger communities, modpacks, and heavy modding."
|
"message": "Ideal for 15–25 players, modpacks, or heavy modding."
|
||||||
},
|
},
|
||||||
"servers.plan.large.name": {
|
"servers.plan.large.name": {
|
||||||
"message": "Large"
|
"message": "Large"
|
||||||
},
|
},
|
||||||
"servers.plan.large.symbol": {
|
|
||||||
"message": "L"
|
|
||||||
},
|
|
||||||
"servers.plan.medium.description": {
|
"servers.plan.medium.description": {
|
||||||
"message": "Great for modded multiplayer and small communities."
|
"message": "Great for 6–15 players and multiple mods."
|
||||||
},
|
},
|
||||||
"servers.plan.medium.name": {
|
"servers.plan.medium.name": {
|
||||||
"message": "Medium"
|
"message": "Medium"
|
||||||
},
|
},
|
||||||
"servers.plan.medium.symbol": {
|
|
||||||
"message": "M"
|
|
||||||
},
|
|
||||||
"servers.plan.small.description": {
|
"servers.plan.small.description": {
|
||||||
"message": "Perfect for vanilla multiplayer, small friend groups, SMPs, and light modding."
|
"message": "Perfect for 1–5 friends with a few light mods."
|
||||||
},
|
},
|
||||||
"servers.plan.small.name": {
|
"servers.plan.small.name": {
|
||||||
"message": "Small"
|
"message": "Small"
|
||||||
},
|
},
|
||||||
"servers.plan.small.symbol": {
|
|
||||||
"message": "S"
|
|
||||||
},
|
|
||||||
"settings.billing.modal.cancel.action": {
|
"settings.billing.modal.cancel.action": {
|
||||||
"message": "Cancel subscription"
|
"message": "Cancel subscription"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<h1 class="wrap-as-needed">
|
<h1 class="wrap-as-needed">
|
||||||
{{ project.title }}
|
{{ project.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<Badge :type="project.status" />
|
<ProjectStatusBadge :status="project.status" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2>Project settings</h2>
|
<h2>Project settings</h2>
|
||||||
@@ -870,6 +870,7 @@ import {
|
|||||||
ProjectSidebarCreators,
|
ProjectSidebarCreators,
|
||||||
ProjectSidebarDetails,
|
ProjectSidebarDetails,
|
||||||
ProjectSidebarLinks,
|
ProjectSidebarLinks,
|
||||||
|
ProjectStatusBadge,
|
||||||
ScrollablePanel,
|
ScrollablePanel,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
@@ -880,7 +881,6 @@ import dayjs from "dayjs";
|
|||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import Accordion from "~/components/ui/Accordion.vue";
|
||||||
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||||
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
||||||
|
|||||||
@@ -242,8 +242,7 @@
|
|||||||
import { formatProjectStatus } from "@modrinth/utils";
|
import { formatProjectStatus } from "@modrinth/utils";
|
||||||
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
import { UploadIcon, SaveIcon, TrashIcon, XIcon, IssuesIcon, CheckIcon } from "@modrinth/assets";
|
||||||
import { Multiselect } from "vue-multiselect";
|
import { Multiselect } from "vue-multiselect";
|
||||||
import { ConfirmModal } from "@modrinth/ui";
|
import { ConfirmModal, Avatar } from "@modrinth/ui";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import FileInput from "~/components/ui/FileInput.vue";
|
import FileInput from "~/components/ui/FileInput.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -630,7 +630,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { ButtonStyled, ConfirmModal, MarkdownEditor } from "@modrinth/ui";
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
CopyCode,
|
||||||
|
Checkbox,
|
||||||
|
ButtonStyled,
|
||||||
|
ConfirmModal,
|
||||||
|
MarkdownEditor,
|
||||||
|
} from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
FileIcon,
|
FileIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
@@ -656,13 +664,9 @@ import { renderHighlightedString } from "~/helpers/highlight.js";
|
|||||||
import { reportVersion } from "~/utils/report-helpers.ts";
|
import { reportVersion } from "~/utils/report-helpers.ts";
|
||||||
import { useImageUpload } from "~/composables/image-upload.ts";
|
import { useImageUpload } from "~/composables/image-upload.ts";
|
||||||
|
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
|
||||||
import Categories from "~/components/ui/search/Categories.vue";
|
import Categories from "~/components/ui/search/Categories.vue";
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
|
||||||
import FileInput from "~/components/ui/FileInput.vue";
|
import FileInput from "~/components/ui/FileInput.vue";
|
||||||
import Modal from "~/components/ui/Modal.vue";
|
import Modal from "~/components/ui/Modal.vue";
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ import { useVIntl } from "@vintl/vintl";
|
|||||||
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
import type { ServerNotice as ServerNoticeType } from "@modrinth/utils";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
|
import { NOTICE_LEVELS } from "@modrinth/ui/src/utils/notices.ts";
|
||||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
|
import AssignNoticeModal from "~/components/ui/servers/notice/AssignNoticeModal.vue";
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
@@ -290,7 +290,7 @@ const assignNoticeModal = ref<InstanceType<typeof AssignNoticeModal>>();
|
|||||||
await refreshNotices();
|
await refreshNotices();
|
||||||
|
|
||||||
async function refreshNotices() {
|
async function refreshNotices() {
|
||||||
await usePyroFetch("notices").then((res) => {
|
await useServersFetch("notices").then((res) => {
|
||||||
notices.value = res as ServerNoticeType[];
|
notices.value = res as ServerNoticeType[];
|
||||||
notices.value.sort((a, b) => {
|
notices.value.sort((a, b) => {
|
||||||
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at));
|
const dateDiff = dayjs(b.announce_at).diff(dayjs(a.announce_at));
|
||||||
@@ -347,7 +347,7 @@ function startEditing(notice: ServerNoticeType, assignments: boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNotice(notice: ServerNoticeType) {
|
async function deleteNotice(notice: ServerNoticeType) {
|
||||||
await usePyroFetch(`notices/${notice.id}`, {
|
await useServersFetch(`notices/${notice.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -401,7 +401,7 @@ async function saveChanges() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await usePyroFetch(`notices/${editingNotice.value?.id}`, {
|
await useServersFetch(`notices/${editingNotice.value?.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: {
|
body: {
|
||||||
message: newNoticeMessage.value,
|
message: newNoticeMessage.value,
|
||||||
@@ -432,7 +432,7 @@ async function createNotice() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await usePyroFetch("notices", {
|
await useServersFetch("notices", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
message: newNoticeMessage.value,
|
message: newNoticeMessage.value,
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ import {
|
|||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
import { Avatar, Checkbox, Badge } from "@modrinth/ui";
|
||||||
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import PrismIcon from "~/assets/images/external/prism.svg?component";
|
import PrismIcon from "~/assets/images/external/prism.svg?component";
|
||||||
import ATLauncher from "~/assets/images/external/atlauncher.svg?component";
|
import ATLauncher from "~/assets/images/external/atlauncher.svg?component";
|
||||||
import CurseForge from "~/assets/images/external/curseforge.svg?component";
|
import CurseForge from "~/assets/images/external/curseforge.svg?component";
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
|
||||||
|
|
||||||
import { homePageProjects } from "~/generated/state.json";
|
import { homePageProjects } from "~/generated/state.json";
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets";
|
import { ChevronRightIcon, HistoryIcon } from "@modrinth/assets";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
import { Avatar } from "@modrinth/ui";
|
||||||
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
||||||
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts";
|
import { fetchExtraNotificationData, groupNotifications } from "~/helpers/notifications.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -49,12 +49,14 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<p v-else>You don't have any unread notifications.</p>
|
<p v-else>You don't have any unread notifications.</p>
|
||||||
<Pagination :page="page" :count="pages" @switch-page="changePage" />
|
<div class="flex justify-end">
|
||||||
|
<Pagination :page="page" :count="pages" @switch-page="changePage" />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Chips } from "@modrinth/ui";
|
import { Button, Pagination, Chips } from "@modrinth/ui";
|
||||||
import { HistoryIcon, CheckCheckIcon } from "@modrinth/assets";
|
import { HistoryIcon, CheckCheckIcon } from "@modrinth/assets";
|
||||||
import {
|
import {
|
||||||
fetchExtraNotificationData,
|
fetchExtraNotificationData,
|
||||||
@@ -63,7 +65,6 @@ import {
|
|||||||
} from "~/helpers/notifications.ts";
|
} from "~/helpers/notifications.ts";
|
||||||
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
import NotificationItem from "~/components/ui/NotificationItem.vue";
|
||||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||||
import Pagination from "~/components/ui/Pagination.vue";
|
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Notifications - Modrinth",
|
title: "Notifications - Modrinth",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<template v-if="orgs?.length > 0">
|
<template v-if="orgs?.length > 0">
|
||||||
<div class="orgs-grid">
|
<div class="orgs-grid">
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-for="org in orgs"
|
v-for="org in sortedOrgs"
|
||||||
:key="org.id"
|
:key="org.id"
|
||||||
:to="`/organization/${org.slug}`"
|
:to="`/organization/${org.slug}`"
|
||||||
class="universal-card button-base recessed org"
|
class="universal-card button-base recessed org"
|
||||||
@@ -67,6 +67,8 @@ const { data: orgs, error } = useAsyncData("organizations", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sortedOrgs = computed(() => orgs.value.sort((a, b) => a.name.localeCompare(b.name)));
|
||||||
|
|
||||||
const onlyAcceptedMembers = (members) => members.filter((member) => member?.accepted);
|
const onlyAcceptedMembers = (members) => members.filter((member) => member?.accepted);
|
||||||
|
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
|
|||||||
@@ -279,18 +279,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Badge v-if="project.status" :type="project.status" class="status" />
|
<ProjectStatusBadge v-if="project.status" :status="project.status" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link
|
<ButtonStyled circular>
|
||||||
class="square-button"
|
<nuxt-link
|
||||||
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
:to="`/${$getProjectTypeForUrl(project.project_type, project.loaders)}/${
|
||||||
project.slug ? project.slug : project.id
|
project.slug ? project.slug : project.id
|
||||||
}/settings`"
|
}/settings`"
|
||||||
>
|
>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,19 +313,23 @@ import {
|
|||||||
SortAscendingIcon as AscendingIcon,
|
SortAscendingIcon as AscendingIcon,
|
||||||
SortDescendingIcon as DescendingIcon,
|
SortDescendingIcon as DescendingIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { commonMessages } from "@modrinth/ui";
|
import {
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
CopyCode,
|
||||||
|
ProjectStatusBadge,
|
||||||
|
commonMessages,
|
||||||
|
} from "@modrinth/ui";
|
||||||
|
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
|
||||||
import Modal from "~/components/ui/Modal.vue";
|
import Modal from "~/components/ui/Modal.vue";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
import ModalCreation from "~/components/ui/ModalCreation.vue";
|
||||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
ButtonStyled,
|
||||||
|
ProjectStatusBadge,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
|||||||
@@ -527,7 +527,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Multiselect } from "vue-multiselect";
|
import { Multiselect } from "vue-multiselect";
|
||||||
import { ButtonStyled, useRelativeTime } from "@modrinth/ui";
|
import { Avatar, ButtonStyled, useRelativeTime } from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
CompassIcon,
|
CompassIcon,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
@@ -539,7 +539,6 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import PrismLauncherLogo from "~/assets/images/external/prism.svg?component";
|
import PrismLauncherLogo from "~/assets/images/external/prism.svg?component";
|
||||||
import ATLauncherLogo from "~/assets/images/external/atlauncher.svg?component";
|
import ATLauncherLogo from "~/assets/images/external/atlauncher.svg?component";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||||
|
|
||||||
import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json";
|
import { homePageProjects, homePageSearch, homePageNotifs } from "~/generated/state.json";
|
||||||
|
|||||||
@@ -81,7 +81,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mobile-row">
|
<div class="mobile-row">
|
||||||
is requesting to be
|
is requesting to be
|
||||||
<Badge :type="project.requested_status ? project.requested_status : 'approved'" />
|
<ProjectStatusBadge
|
||||||
|
:status="project.requested_status ? project.requested_status : 'approved'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -103,7 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Chips, useRelativeTime } from "@modrinth/ui";
|
import { Avatar, ProjectStatusBadge, Chips, useRelativeTime } from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
UnknownIcon,
|
UnknownIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
@@ -112,9 +114,8 @@ import {
|
|||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import { formatProjectType } from "~/plugins/shorthands.js";
|
import { formatProjectType } from "~/plugins/shorthands.js";
|
||||||
|
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Review projects - Modrinth",
|
title: "Review projects - Modrinth",
|
||||||
@@ -170,28 +171,6 @@ const projectTypes = computed(() => {
|
|||||||
return [...set];
|
return [...set];
|
||||||
});
|
});
|
||||||
|
|
||||||
function segmentData(data, segmentSize = 800) {
|
|
||||||
return data.reduce((acc, curr, index) => {
|
|
||||||
const segment = Math.floor(index / segmentSize);
|
|
||||||
|
|
||||||
if (!acc[segment]) {
|
|
||||||
acc[segment] = [];
|
|
||||||
}
|
|
||||||
acc[segment].push(curr);
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchSegmented(data, createUrl, options = {}) {
|
|
||||||
return Promise.all(segmentData(data).map((ids) => useBaseFetch(createUrl(ids), options))).then(
|
|
||||||
(results) => results.flat(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function asEncodedJsonArray(data) {
|
|
||||||
return encodeURIComponent(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projects.value) {
|
if (projects.value) {
|
||||||
const teamIds = projects.value.map((x) => x.team_id);
|
const teamIds = projects.value.map((x) => x.team_id);
|
||||||
const orgIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
|
const orgIds = projects.value.filter((x) => x.organization).map((x) => x.organization);
|
||||||
|
|||||||
@@ -334,6 +334,7 @@ import {
|
|||||||
ImageIcon,
|
ImageIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||||
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
import ProjectCard from "~/components/ui/ProjectCard.vue";
|
||||||
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
import LogoAnimated from "~/components/brand/LogoAnimated.vue";
|
||||||
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
@@ -388,7 +389,7 @@ async function updateServerContext() {
|
|||||||
if (!auth.value.user) {
|
if (!auth.value.user) {
|
||||||
router.push("/auth/sign-in?redirect=" + encodeURIComponent(route.fullPath));
|
router.push("/auth/sign-in?redirect=" + encodeURIComponent(route.fullPath));
|
||||||
} else if (route.query.sid !== null) {
|
} else if (route.query.sid !== null) {
|
||||||
server.value = await usePyroServer(route.query.sid, ["general", "content"], {
|
server.value = await useModrinthServers(route.query.sid, ["general", "content"], {
|
||||||
waitForModules: true,
|
waitForModules: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -519,7 +520,6 @@ async function serverInstall(project) {
|
|||||||
|
|
||||||
if (projectType.value.id === "modpack") {
|
if (projectType.value.id === "modpack") {
|
||||||
await server.value.general.reinstall(
|
await server.value.general.reinstall(
|
||||||
server.value.serverId,
|
|
||||||
false,
|
false,
|
||||||
project.project_id,
|
project.project_id,
|
||||||
version.id,
|
version.id,
|
||||||
|
|||||||
@@ -4,27 +4,28 @@
|
|||||||
data-pyro
|
data-pyro
|
||||||
class="servers-hero relative isolate -mt-44 h-full min-h-screen pt-8"
|
class="servers-hero relative isolate -mt-44 h-full min-h-screen pt-8"
|
||||||
>
|
>
|
||||||
<PurchaseModal
|
<ModrinthServersPurchaseModal
|
||||||
v-if="showModal && selectedProduct && customer"
|
v-if="customer"
|
||||||
:key="selectedProduct.id"
|
:key="`purchase-modal-${customer.id}`"
|
||||||
ref="purchaseModal"
|
ref="purchaseModal"
|
||||||
:product="selectedProduct"
|
|
||||||
:country="country"
|
|
||||||
:custom-server="customServer"
|
|
||||||
:publishable-key="config.public.stripePublishableKey"
|
:publishable-key="config.public.stripePublishableKey"
|
||||||
:send-billing-request="
|
:initiate-payment="
|
||||||
async (body) =>
|
async (body) =>
|
||||||
await useBaseFetch('billing/payment', { internal: true, method: 'POST', body })
|
await useBaseFetch('billing/payment', { internal: true, method: 'POST', body })
|
||||||
"
|
"
|
||||||
:fetch-payment-data="fetchPaymentData"
|
:available-products="pyroProducts"
|
||||||
:on-error="handleError"
|
:on-error="handleError"
|
||||||
:customer="customer"
|
:customer="customer"
|
||||||
:payment-methods="paymentMethods"
|
:payment-methods="paymentMethods"
|
||||||
|
:currency="selectedCurrency"
|
||||||
:return-url="`${config.public.siteUrl}/servers/manage`"
|
:return-url="`${config.public.siteUrl}/servers/manage`"
|
||||||
:server-name="`${auth?.user?.username}'s server`"
|
:server-name="`${auth?.user?.username}'s server`"
|
||||||
:fetch-capacity-statuses="fetchCapacityStatuses"
|
|
||||||
:out-of-stock-url="outOfStockUrl"
|
:out-of-stock-url="outOfStockUrl"
|
||||||
@hidden="handleModalHidden"
|
:fetch-capacity-statuses="fetchCapacityStatuses"
|
||||||
|
:pings="regionPings"
|
||||||
|
:regions="regions"
|
||||||
|
:refresh-payment-methods="fetchPaymentData"
|
||||||
|
:fetch-stock="fetchStock"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -442,8 +443,8 @@
|
|||||||
Where are Modrinth Servers located? Can I choose a region?
|
Where are Modrinth Servers located? Can I choose a region?
|
||||||
</summary>
|
</summary>
|
||||||
<p class="m-0 ml-6 leading-[160%]">
|
<p class="m-0 ml-6 leading-[160%]">
|
||||||
Currently, Modrinth Servers are located on the east coast of the United States in
|
We have servers in both North America in Vint Hill, Virginia, and Europe in Limburg,
|
||||||
Vint Hill, Virginia. More regions to come in the future!
|
Germany. More regions to come in the future!
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -497,98 +498,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
|
||||||
v-if="false"
|
|
||||||
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
|
||||||
>
|
|
||||||
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
|
||||||
<div class="mx-auto flex w-full max-w-7xl flex-col gap-8">
|
|
||||||
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
|
|
||||||
<div class="flex flex-col gap-8">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div
|
|
||||||
class="relative w-fit rounded-full bg-highlight-green px-3 py-1 text-sm font-bold text-brand backdrop-blur-lg"
|
|
||||||
>
|
|
||||||
Server Locations
|
|
||||||
</div>
|
|
||||||
<h1 class="relative m-0 max-w-2xl text-4xl leading-[120%] md:text-7xl">
|
|
||||||
Coast-to-Coast Coverage
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-8">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="grid size-8 place-content-center rounded-full bg-highlight-green">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="text-brand"
|
|
||||||
>
|
|
||||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
|
|
||||||
<circle cx="12" cy="10" r="3" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
|
|
||||||
US Coverage
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
|
||||||
>
|
|
||||||
With strategically placed servers in New York, California, Texas, Florida, and
|
|
||||||
Washington, we ensure low latency connections for players across North America.
|
|
||||||
Each location is equipped with high-performance hardware and DDoS protection.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="grid size-8 place-content-center rounded-full bg-highlight-blue">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="text-blue"
|
|
||||||
>
|
|
||||||
<path d="M12 2a10 10 0 1 0 10 10" />
|
|
||||||
<path d="M18 13a6 6 0 0 0-6-6" />
|
|
||||||
<path d="M13 2.05a10 10 0 0 1 2 2" />
|
|
||||||
<path d="M19.5 8.5a10 10 0 0 1 2 2" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="relative m-0 text-xl font-medium leading-[155%] md:text-2xl">
|
|
||||||
Global Expansion
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
|
||||||
>
|
|
||||||
We're expanding to Europe and Asia-Pacific regions soon, bringing Modrinth's
|
|
||||||
seamless hosting experience worldwide. Join our Discord to stay updated on new
|
|
||||||
region launches.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Globe />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id="plan"
|
id="plan"
|
||||||
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
class="relative mt-24 flex flex-col bg-[radial-gradient(65%_50%_at_50%_-10%,var(--color-brand-highlight)_0%,var(--color-accent-contrast)_100%)] px-3 pt-24 md:mt-48 md:pt-48"
|
||||||
@@ -596,19 +505,35 @@
|
|||||||
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
<div class="faded-brand-line absolute left-0 top-0 h-[1px] w-full"></div>
|
||||||
<div class="mx-auto flex w-full max-w-7xl flex-col items-center gap-8 text-center">
|
<div class="mx-auto flex w-full max-w-7xl flex-col items-center gap-8 text-center">
|
||||||
<h1 class="relative m-0 text-4xl leading-[120%] md:text-7xl">
|
<h1 class="relative m-0 text-4xl leading-[120%] md:text-7xl">
|
||||||
Start your server on Modrinth
|
There's a server for everyone
|
||||||
</h1>
|
</h1>
|
||||||
<h2
|
<p class="m-0 flex items-center gap-1">
|
||||||
class="relative m-0 max-w-xl text-base font-normal leading-[155%] text-secondary md:text-[18px]"
|
Available in North America and Europe for wide coverage.
|
||||||
>
|
</p>
|
||||||
{{
|
|
||||||
isAtCapacity && !loggedOut
|
|
||||||
? "We are currently at capacity. Please try again later."
|
|
||||||
: "There's a plan for everyone! Choose the one that fits your needs."
|
|
||||||
}}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<ul class="m-0 mt-8 flex w-full flex-col gap-8 p-0 lg:flex-row">
|
<div class="grid grid-cols-[1fr_auto_1fr] items-center gap-3">
|
||||||
|
<span></span>
|
||||||
|
<OptionGroup v-slot="{ option }" v-model="billingPeriod" :options="billingPeriods">
|
||||||
|
<template v-if="option === 'monthly'"> Pay monthly </template>
|
||||||
|
<span v-else-if="option === 'quarterly'"> Pay quarterly </span>
|
||||||
|
<span v-else-if="option === 'yearly'"> Pay yearly </span>
|
||||||
|
</OptionGroup>
|
||||||
|
<template v-if="billingPeriods.includes('quarterly')">
|
||||||
|
<button
|
||||||
|
v-if="billingPeriod !== 'quarterly'"
|
||||||
|
class="bg-transparent p-0 text-sm font-medium text-brand hover:underline active:scale-95"
|
||||||
|
@click="billingPeriod = 'quarterly'"
|
||||||
|
>
|
||||||
|
Save 16% with quarterly billing!
|
||||||
|
</button>
|
||||||
|
<span v-else class="bg-transparent p-0 text-sm font-medium text-brand">
|
||||||
|
Save 16% with quarterly billing!
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<span v-else></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="m-0 flex w-full grid-cols-3 flex-col gap-8 p-0 lg:grid">
|
||||||
<ServerPlanSelector
|
<ServerPlanSelector
|
||||||
:capacity="capacityStatuses?.small?.available"
|
:capacity="capacityStatuses?.small?.available"
|
||||||
plan="small"
|
plan="small"
|
||||||
@@ -616,9 +541,12 @@
|
|||||||
:storage="plans.small.metadata.storage"
|
:storage="plans.small.metadata.storage"
|
||||||
:cpus="plans.small.metadata.cpu"
|
:cpus="plans.small.metadata.cpu"
|
||||||
:price="
|
:price="
|
||||||
plans.small?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
plans.small?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||||
?.monthly
|
?.intervals?.[billingPeriod]
|
||||||
"
|
"
|
||||||
|
:interval="billingPeriod"
|
||||||
|
:currency="selectedCurrency"
|
||||||
|
:is-usa="country.toLowerCase() === 'us'"
|
||||||
@select="selectProduct('small')"
|
@select="selectProduct('small')"
|
||||||
@scroll-to-faq="scrollToFaq()"
|
@scroll-to-faq="scrollToFaq()"
|
||||||
/>
|
/>
|
||||||
@@ -629,9 +557,12 @@
|
|||||||
:storage="plans.medium.metadata.storage"
|
:storage="plans.medium.metadata.storage"
|
||||||
:cpus="plans.medium.metadata.cpu"
|
:cpus="plans.medium.metadata.cpu"
|
||||||
:price="
|
:price="
|
||||||
plans.medium?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
plans.medium?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||||
?.monthly
|
?.intervals?.[billingPeriod]
|
||||||
"
|
"
|
||||||
|
:interval="billingPeriod"
|
||||||
|
:currency="selectedCurrency"
|
||||||
|
:is-usa="country.toLowerCase() === 'us'"
|
||||||
@select="selectProduct('medium')"
|
@select="selectProduct('medium')"
|
||||||
@scroll-to-faq="scrollToFaq()"
|
@scroll-to-faq="scrollToFaq()"
|
||||||
/>
|
/>
|
||||||
@@ -641,10 +572,13 @@
|
|||||||
:storage="plans.large.metadata.storage"
|
:storage="plans.large.metadata.storage"
|
||||||
:cpus="plans.large.metadata.cpu"
|
:cpus="plans.large.metadata.cpu"
|
||||||
:price="
|
:price="
|
||||||
plans.large?.prices?.find((x) => x.currency_code === 'USD')?.prices?.intervals
|
plans.large?.prices?.find((x) => x.currency_code === selectedCurrency)?.prices
|
||||||
?.monthly
|
?.intervals?.[billingPeriod]
|
||||||
"
|
"
|
||||||
|
:currency="selectedCurrency"
|
||||||
|
:is-usa="country.toLowerCase() === 'us'"
|
||||||
plan="large"
|
plan="large"
|
||||||
|
:interval="billingPeriod"
|
||||||
@select="selectProduct('large')"
|
@select="selectProduct('large')"
|
||||||
@scroll-to-faq="scrollToFaq()"
|
@scroll-to-faq="scrollToFaq()"
|
||||||
/>
|
/>
|
||||||
@@ -654,10 +588,9 @@
|
|||||||
class="mb-24 flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0"
|
class="mb-24 flex w-full flex-col items-start justify-between gap-4 rounded-2xl bg-bg p-8 text-left lg:flex-row lg:gap-0"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<h1 class="m-0">Build your own</h1>
|
<h1 class="m-0">Know exactly what you need?</h1>
|
||||||
<h2 class="m-0 text-base font-normal text-primary">
|
<h2 class="m-0 text-base font-normal text-primary">
|
||||||
If you're a more technical server administrator, you can pick your own RAM and storage
|
Pick a customized plan with just the specs you need.
|
||||||
options.
|
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -666,7 +599,7 @@
|
|||||||
>
|
>
|
||||||
<ButtonStyled color="standard" size="large">
|
<ButtonStyled color="standard" size="large">
|
||||||
<button class="w-full md:w-fit" @click="selectProduct('custom')">
|
<button class="w-full md:w-fit" @click="selectProduct('custom')">
|
||||||
Build your own
|
Get started
|
||||||
<RightArrowIcon class="shrink-0" />
|
<RightArrowIcon class="shrink-0" />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -679,7 +612,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ButtonStyled, PurchaseModal } from "@modrinth/ui";
|
import { ButtonStyled, ModrinthServersPurchaseModal } from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
GameIcon,
|
GameIcon,
|
||||||
@@ -690,9 +623,13 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { products } from "~/generated/state.json";
|
import { products } from "~/generated/state.json";
|
||||||
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
import LoaderIcon from "~/components/ui/servers/icons/LoaderIcon.vue";
|
||||||
import Globe from "~/components/ui/servers/Globe.vue";
|
|
||||||
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
import ServerPlanSelector from "~/components/ui/servers/marketing/ServerPlanSelector.vue";
|
||||||
|
import OptionGroup from "~/components/ui/OptionGroup.vue";
|
||||||
|
|
||||||
|
const billingPeriods = ref(["monthly", "quarterly"]);
|
||||||
|
const billingPeriod = ref(billingPeriods.value.includes("quarterly") ? "quarterly" : "monthly");
|
||||||
|
|
||||||
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
|
const pyroProducts = products.filter((p) => p.metadata.type === "pyro");
|
||||||
const pyroPlanProducts = pyroProducts.filter(
|
const pyroPlanProducts = pyroProducts.filter(
|
||||||
@@ -711,16 +648,6 @@ useSeoMeta({
|
|||||||
ogDescription: description,
|
ogDescription: description,
|
||||||
});
|
});
|
||||||
|
|
||||||
useHead({
|
|
||||||
script: [
|
|
||||||
{
|
|
||||||
src: "https://js.stripe.com/v3/",
|
|
||||||
defer: true,
|
|
||||||
async: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
@@ -740,6 +667,7 @@ const isDeleting = ref(false);
|
|||||||
const typingSpeed = 75;
|
const typingSpeed = 75;
|
||||||
const deletingSpeed = 25;
|
const deletingSpeed = 25;
|
||||||
const pauseTime = 2000;
|
const pauseTime = 2000;
|
||||||
|
const selectedCurrency = ref("USD");
|
||||||
|
|
||||||
const loggedOut = computed(() => !auth.value.user);
|
const loggedOut = computed(() => !auth.value.user);
|
||||||
const outOfStockUrl = "https://discord.modrinth.com";
|
const outOfStockUrl = "https://discord.modrinth.com";
|
||||||
@@ -747,13 +675,23 @@ const outOfStockUrl = "https://discord.modrinth.com";
|
|||||||
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
|
const { data: hasServers } = await useAsyncData("ServerListCountCheck", async () => {
|
||||||
try {
|
try {
|
||||||
if (!auth.value.user) return false;
|
if (!auth.value.user) return false;
|
||||||
const response = await usePyroFetch("servers");
|
const response = await useServersFetch("servers");
|
||||||
return response.servers && response.servers.length > 0;
|
return response.servers && response.servers.length > 0;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function fetchStock(region, request) {
|
||||||
|
return useServersFetch(`stock?region=${region.shortcode}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
...request,
|
||||||
|
},
|
||||||
|
bypassAuth: true,
|
||||||
|
}).then((res) => res.available);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchCapacityStatuses(customProduct = null) {
|
async function fetchCapacityStatuses(customProduct = null) {
|
||||||
try {
|
try {
|
||||||
const productsToCheck = customProduct?.metadata
|
const productsToCheck = customProduct?.metadata
|
||||||
@@ -765,7 +703,7 @@ async function fetchCapacityStatuses(customProduct = null) {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
const capacityChecks = productsToCheck.map((product) =>
|
const capacityChecks = productsToCheck.map((product) =>
|
||||||
usePyroFetch("stock", {
|
useServersFetch("stock", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
cpu: product.metadata.cpu,
|
cpu: product.metadata.cpu,
|
||||||
@@ -841,23 +779,6 @@ const handleError = (err) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModalHidden = () => {
|
|
||||||
showModal.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(selectedProduct, async (newProduct) => {
|
|
||||||
if (newProduct) {
|
|
||||||
showModal.value = false;
|
|
||||||
await nextTick();
|
|
||||||
showModal.value = true;
|
|
||||||
modalKey.value++;
|
|
||||||
await nextTick();
|
|
||||||
if (purchaseModal.value && purchaseModal.value.show) {
|
|
||||||
purchaseModal.value.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchPaymentData() {
|
async function fetchPaymentData() {
|
||||||
if (!auth.value.user) return;
|
if (!auth.value.user) return;
|
||||||
try {
|
try {
|
||||||
@@ -954,8 +875,10 @@ const selectProduct = async (product) => {
|
|||||||
modalKey.value++;
|
modalKey.value++;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
if (purchaseModal.value && purchaseModal.value.show) {
|
if (product === "custom") {
|
||||||
purchaseModal.value.show();
|
purchaseModal.value?.show(billingPeriod.value);
|
||||||
|
} else {
|
||||||
|
purchaseModal.value?.show(billingPeriod.value, selectedProduct.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -966,9 +889,82 @@ const planQuery = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const regions = ref([]);
|
||||||
|
const regionPings = ref([]);
|
||||||
|
|
||||||
|
function pingRegions() {
|
||||||
|
useServersFetch("regions", {
|
||||||
|
method: "GET",
|
||||||
|
version: 1,
|
||||||
|
bypassAuth: true,
|
||||||
|
}).then((res) => {
|
||||||
|
regions.value = res;
|
||||||
|
regions.value.forEach((region) => {
|
||||||
|
runPingTest(region);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const PING_COUNT = 20;
|
||||||
|
const PING_INTERVAL = 200;
|
||||||
|
const MAX_PING_TIME = 1000;
|
||||||
|
|
||||||
|
function runPingTest(region, index = 1) {
|
||||||
|
if (index > 10) {
|
||||||
|
regionPings.value.push({
|
||||||
|
region: region.shortcode,
|
||||||
|
ping: -1,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = `wss://${region.shortcode}${index}.${region.zone}/pingtest`;
|
||||||
|
try {
|
||||||
|
const socket = new WebSocket(wsUrl);
|
||||||
|
const pings = [];
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
for (let i = 0; i < PING_COUNT; i++) {
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.send(performance.now());
|
||||||
|
}, i * PING_INTERVAL);
|
||||||
|
}
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
socket.close();
|
||||||
|
|
||||||
|
const median = Math.round([...pings].sort((a, b) => a - b)[Math.floor(pings.length / 2)]);
|
||||||
|
if (median) {
|
||||||
|
regionPings.value.push({
|
||||||
|
region: region.shortcode,
|
||||||
|
ping: median,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PING_COUNT * PING_INTERVAL + MAX_PING_TIME,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
pings.push(performance.now() - event.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (event) => {
|
||||||
|
console.error(
|
||||||
|
`Failed to connect pingtest WebSocket with ${wsUrl}, trying index ${index + 1}:`,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
runPingTest(region, index + 1);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to connect pingtest WebSocket with ${wsUrl}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
startTyping();
|
startTyping();
|
||||||
planQuery();
|
planQuery();
|
||||||
|
pingRegions();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(customer, (newCustomer) => {
|
watch(customer, (newCustomer) => {
|
||||||
|
|||||||
@@ -18,160 +18,96 @@
|
|||||||
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
|
v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<ErrorInformationCard
|
||||||
<div class="flex flex-col items-center text-center">
|
title="Server upgrading"
|
||||||
<div class="flex flex-col items-center gap-4">
|
description="Your server's hardware is currently being upgraded and will be back online shortly!"
|
||||||
<div class="grid place-content-center rounded-full bg-bg-blue p-4">
|
:icon="TransferIcon"
|
||||||
<TransferIcon class="size-12 text-blue" />
|
icon-color="blue"
|
||||||
</div>
|
:action="generalErrorAction"
|
||||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server upgrading</h1>
|
/>
|
||||||
</div>
|
|
||||||
<p class="text-lg text-secondary">
|
|
||||||
Your server's hardware is currently being upgraded and will be back online shortly!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="serverData?.status === 'suspended'"
|
v-else-if="serverData?.status === 'suspended'"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<ErrorInformationCard
|
||||||
<div class="flex flex-col items-center text-center">
|
title="Server suspended"
|
||||||
<div class="flex flex-col items-center gap-4">
|
:description="suspendedDescription"
|
||||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
:icon="LockIcon"
|
||||||
<LockIcon class="size-12 text-orange" />
|
icon-color="orange"
|
||||||
</div>
|
:action="suspendedAction"
|
||||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server suspended</h1>
|
/>
|
||||||
</div>
|
|
||||||
<p class="text-lg text-secondary">
|
|
||||||
{{
|
|
||||||
serverData.suspension_reason === "cancelled"
|
|
||||||
? "Your subscription has been cancelled."
|
|
||||||
: serverData.suspension_reason
|
|
||||||
? `Your server has been suspended: ${serverData.suspension_reason}`
|
|
||||||
: "Your server has been suspended."
|
|
||||||
}}
|
|
||||||
<br />
|
|
||||||
Contact Modrinth Support if you believe this is an error.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/settings/billing')">
|
|
||||||
<button class="mt-6 !w-full">Go to billing settings</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="
|
v-else-if="
|
||||||
server.general?.error?.error.statusCode === 403 ||
|
server.moduleErrors?.general?.error.statusCode === 403 ||
|
||||||
server.general?.error?.error.statusCode === 404
|
server.moduleErrors?.general?.error.statusCode === 404
|
||||||
"
|
"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<ErrorInformationCard
|
||||||
<div class="flex flex-col items-center text-center">
|
title="An error occured."
|
||||||
<div class="flex flex-col items-center gap-4">
|
description="Please contact Modrinth Support."
|
||||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
:icon="TransferIcon"
|
||||||
<TransferIcon class="size-12 text-orange" />
|
icon-color="orange"
|
||||||
</div>
|
:error-details="generalErrorDetails"
|
||||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Server not found</h1>
|
:action="generalErrorAction"
|
||||||
</div>
|
/>
|
||||||
<p class="text-lg text-secondary">
|
|
||||||
You don't have permission to view this server or it no longer exists. If you believe this
|
|
||||||
is an error, please contact Modrinth Support.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
|
||||||
|
|
||||||
<ButtonStyled size="large" color="brand" @click="() => router.push('/servers/manage')">
|
|
||||||
<button class="mt-6 !w-full">Go back to all servers</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="server.general?.error?.error.statusCode === 503"
|
v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<ErrorInformationCard
|
||||||
<div class="flex flex-col items-center text-center">
|
title="Server Node Unavailable"
|
||||||
<div class="flex flex-col items-center gap-4">
|
:icon="PanelErrorIcon"
|
||||||
<div class="grid place-content-center rounded-full bg-bg-red p-4">
|
icon-color="red"
|
||||||
<UiServersIconsPanelErrorIcon class="size-12 text-red" />
|
:action="nodeUnavailableAction"
|
||||||
</div>
|
:error-details="nodeUnavailableDetails"
|
||||||
<h1 class="m-0 mb-4 w-fit text-4xl font-bold">Server Node Unavailable</h1>
|
>
|
||||||
|
<template #description>
|
||||||
|
<div class="text-md space-y-4">
|
||||||
|
<p class="leading-[170%] text-secondary">
|
||||||
|
Your server's node, where your Modrinth Server is physically hosted, is experiencing
|
||||||
|
issues. We are working with our datacenter to resolve the issue as quickly as possible.
|
||||||
|
</p>
|
||||||
|
<p class="leading-[170%] text-secondary">
|
||||||
|
Your data is safe and will not be lost, and your server will be back online as soon as
|
||||||
|
the issue is resolved.
|
||||||
|
</p>
|
||||||
|
<p class="leading-[170%] text-secondary">
|
||||||
|
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
|
||||||
|
bubble in the bottom right corner and we'll be happy to help.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
</template>
|
||||||
Your server's node, where your Modrinth Server is physically hosted, is experiencing
|
</ErrorInformationCard>
|
||||||
issues. We are working with our datacenter to resolve the issue as quickly as possible.
|
|
||||||
</p>
|
|
||||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
|
||||||
Your data is safe and will not be lost, and your server will be back online as soon as the
|
|
||||||
issue is resolved.
|
|
||||||
</p>
|
|
||||||
<p class="m-0 mb-4 leading-[170%] text-secondary">
|
|
||||||
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
|
|
||||||
bubble in the bottom right corner and we'll be happy to help.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<UiCopyCode :text="'Server ID: ' + server.serverId" />
|
|
||||||
<UiCopyCode :text="'Node: ' + server.general?.datacenter" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ButtonStyled
|
|
||||||
size="large"
|
|
||||||
color="standard"
|
|
||||||
@click="
|
|
||||||
() =>
|
|
||||||
navigateTo('https://discord.modrinth.com', {
|
|
||||||
external: true,
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<button class="mt-6 !w-full">Join Modrinth Discord</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled
|
|
||||||
:disabled="formattedTime !== '00'"
|
|
||||||
size="large"
|
|
||||||
color="standard"
|
|
||||||
@click="() => reloadNuxtApp()"
|
|
||||||
>
|
|
||||||
<button class="mt-3 !w-full">Reload</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="server.general?.error"
|
v-else-if="server.moduleErrors?.general?.error"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<div class="flex max-w-lg flex-col items-center rounded-3xl bg-bg-raised p-6 shadow-xl">
|
<ErrorInformationCard
|
||||||
<div class="flex flex-col items-center text-center">
|
title="Connection lost"
|
||||||
<div class="flex flex-col items-center gap-4">
|
description=""
|
||||||
<div class="grid place-content-center rounded-full bg-bg-orange p-4">
|
:icon="TransferIcon"
|
||||||
<TransferIcon class="size-12 text-orange" />
|
icon-color="orange"
|
||||||
</div>
|
:action="connectionLostAction"
|
||||||
<h1 class="m-0 mb-2 w-fit text-4xl font-bold">Connection lost</h1>
|
>
|
||||||
|
<template #description>
|
||||||
|
<div class="space-y-4">
|
||||||
<div class="text-center text-secondary">
|
<div class="text-center text-secondary">
|
||||||
{{
|
{{
|
||||||
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
|
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-lg text-secondary">
|
||||||
|
Something went wrong, and we couldn't connect to your server. This is likely due to a
|
||||||
|
temporary network issue. You'll be reconnected automatically.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg text-secondary">
|
</template>
|
||||||
Something went wrong, and we couldn't connect to your server. This is likely due to a
|
</ErrorInformationCard>
|
||||||
temporary network issue. You'll be reconnected automatically.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<UiCopyCode :text="JSON.stringify(server.general?.error)" />
|
|
||||||
<ButtonStyled
|
|
||||||
:disabled="formattedTime !== '00'"
|
|
||||||
size="large"
|
|
||||||
color="brand"
|
|
||||||
@click="() => reloadNuxtApp()"
|
|
||||||
>
|
|
||||||
<button class="mt-6 !w-full">Reload</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- SERVER START -->
|
<!-- SERVER START -->
|
||||||
<div
|
<div
|
||||||
@@ -207,6 +143,7 @@
|
|||||||
class="server-action-buttons-anim flex w-fit flex-shrink-0"
|
class="server-action-buttons-anim flex w-fit flex-shrink-0"
|
||||||
>
|
>
|
||||||
<UiServersPanelServerActionButton
|
<UiServersPanelServerActionButton
|
||||||
|
v-if="!serverData.flows?.intro"
|
||||||
class="flex-shrink-0"
|
class="flex-shrink-0"
|
||||||
:is-online="isServerRunning"
|
:is-online="isServerRunning"
|
||||||
:is-actioning="isActioning"
|
:is-actioning="isActioning"
|
||||||
@@ -220,7 +157,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="serverData.flows?.intro"
|
||||||
|
class="flex items-center gap-2 font-semibold text-secondary"
|
||||||
|
>
|
||||||
|
<SettingsIcon /> Configuring server...
|
||||||
|
</div>
|
||||||
<UiServersServerInfoLabels
|
<UiServersServerInfoLabels
|
||||||
|
v-else
|
||||||
:server-data="serverData"
|
:server-data="serverData"
|
||||||
:show-game-label="showGameLabel"
|
:show-game-label="showGameLabel"
|
||||||
:show-loader-label="showLoaderLabel"
|
:show-loader-label="showLoaderLabel"
|
||||||
@@ -231,149 +175,189 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<template v-if="serverData.flows?.intro">
|
||||||
data-pyro-navigation
|
|
||||||
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
|
||||||
>
|
|
||||||
<UiNavTabs :links="navLinks" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-pyro-mount class="h-full w-full flex-1">
|
|
||||||
<div
|
<div
|
||||||
v-if="error"
|
v-if="serverData?.status === 'installing'"
|
||||||
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
class="w-50 h-50 flex items-center justify-center gap-2 text-center text-lg font-bold"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row gap-4">
|
<LazyUiServersPanelSpinner class="size-10 animate-spin" /> Setting up your server...
|
||||||
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
</div>
|
||||||
<div class="flex flex-col gap-2 leading-[150%]">
|
<div v-else>
|
||||||
<div class="flex items-center gap-3">
|
<h2 class="my-4 text-xl font-extrabold">
|
||||||
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
What would you like to install on your new server?
|
||||||
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
</h2>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="errorTitle.toLocaleLowerCase() === 'installation error'" class="font-normal">
|
<ServerInstallation
|
||||||
<div
|
:server="server as ModrinthServer"
|
||||||
v-if="errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'"
|
:backup-in-progress="backupInProgress"
|
||||||
>
|
ignore-current-installation
|
||||||
An invalid loader or Minecraft version was specified and could not be installed.
|
@reinstall="onReinstall"
|
||||||
<ul class="m-0 mt-4 p-0 pl-4">
|
/>
|
||||||
<li>
|
</div>
|
||||||
If this version of Minecraft was released recently, please check if Modrinth
|
</template>
|
||||||
Servers supports it.
|
|
||||||
</li>
|
<template v-else>
|
||||||
<li>
|
<div
|
||||||
If you've installed a modpack, it may have been packaged incorrectly or may not
|
data-pyro-navigation
|
||||||
be compatible with the loader.
|
class="isolate flex w-full select-none flex-col justify-between gap-4 overflow-auto md:flex-row md:items-center"
|
||||||
</li>
|
>
|
||||||
<li>
|
<UiNavTabs :links="navLinks" />
|
||||||
Your server may need to be reinstalled with a valid mod loader and version. You
|
</div>
|
||||||
can change the loader by clicking the "Change Loader" button.
|
|
||||||
</li>
|
<div data-pyro-mount class="h-full w-full flex-1">
|
||||||
<li>
|
<div
|
||||||
If you're stuck, please contact Modrinth Support with the information below:
|
v-if="error"
|
||||||
</li>
|
class="mx-auto mb-4 flex justify-between gap-2 rounded-2xl border-2 border-solid border-red bg-bg-red p-4 font-semibold text-contrast"
|
||||||
</ul>
|
>
|
||||||
<ButtonStyled>
|
<div class="flex flex-row gap-4">
|
||||||
<button class="mt-2" @click="copyServerDebugInfo">
|
<IssuesIcon class="hidden h-8 w-8 shrink-0 text-red sm:block" />
|
||||||
<CopyIcon v-if="!copied" />
|
<div class="flex flex-col gap-2 leading-[150%]">
|
||||||
<CheckIcon v-else />
|
<div class="flex items-center gap-3">
|
||||||
Copy Debug Info
|
<IssuesIcon class="flex h-8 w-8 shrink-0 text-red sm:hidden" />
|
||||||
</button>
|
<div class="flex gap-2 text-2xl font-bold">{{ errorTitle }}</div>
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
|
||||||
An internal error occurred while installing your server. Don't fret — try
|
|
||||||
reinstalling your server, and if the problem persists, please contact Modrinth
|
|
||||||
support with your server's debug information.
|
|
||||||
</div>
|
|
||||||
<div v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'">
|
|
||||||
An error occurred while installing your server because Modrinth Servers does not
|
|
||||||
support the version of Minecraft or the loader you specified. Try reinstalling your
|
|
||||||
server with a different version or loader, and if the problem persists, please
|
|
||||||
contact Modrinth Support with your server's debug information.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="errorTitle === 'Installation error'"
|
v-if="errorTitle.toLocaleLowerCase() === 'installation error'"
|
||||||
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
class="font-normal"
|
||||||
>
|
>
|
||||||
<ButtonStyled v-if="errorLog">
|
<div
|
||||||
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
v-if="
|
||||||
</ButtonStyled>
|
errorMessage.toLocaleLowerCase() === 'the specified version may be incorrect'
|
||||||
<ButtonStyled>
|
"
|
||||||
<button @click="copyServerDebugInfo">
|
>
|
||||||
<CopyIcon v-if="!copied" />
|
An invalid loader or Minecraft version was specified and could not be installed.
|
||||||
<CheckIcon v-else />
|
<ul class="m-0 mt-4 p-0 pl-4">
|
||||||
Copy Debug Info
|
<li>
|
||||||
</button>
|
If this version of Minecraft was released recently, please check if Modrinth
|
||||||
</ButtonStyled>
|
Servers supports it.
|
||||||
<ButtonStyled color="red" type="standard">
|
</li>
|
||||||
<NuxtLink
|
<li>
|
||||||
class="whitespace-pre"
|
If you've installed a modpack, it may have been packaged incorrectly or may
|
||||||
:to="`/servers/manage/${serverId}/options/loader`"
|
not be compatible with the loader.
|
||||||
>
|
</li>
|
||||||
<RightArrowIcon />
|
<li>
|
||||||
Change Loader
|
Your server may need to be reinstalled with a valid mod loader and version.
|
||||||
</NuxtLink>
|
You can change the loader by clicking the "Change Loader" button.
|
||||||
</ButtonStyled>
|
</li>
|
||||||
|
<li>
|
||||||
|
If you're stuck, please contact Modrinth Support with the information below:
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button class="mt-2" @click="copyServerDebugInfo">
|
||||||
|
<CopyIcon v-if="!copied" />
|
||||||
|
<CheckIcon v-else />
|
||||||
|
Copy Debug Info
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<div v-if="errorMessage.toLocaleLowerCase() === 'internal error'">
|
||||||
|
An internal error occurred while installing your server. Don't fret — try
|
||||||
|
reinstalling your server, and if the problem persists, please contact Modrinth
|
||||||
|
support with your server's debug information.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="errorMessage.toLocaleLowerCase() === 'this version is not yet supported'"
|
||||||
|
>
|
||||||
|
An error occurred while installing your server because Modrinth Servers does not
|
||||||
|
support the version of Minecraft or the loader you specified. Try reinstalling
|
||||||
|
your server with a different version or loader, and if the problem persists,
|
||||||
|
please contact Modrinth Support with your server's debug information.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="errorTitle === 'Installation error'"
|
||||||
|
class="mt-2 flex flex-col gap-4 sm:flex-row"
|
||||||
|
>
|
||||||
|
<ButtonStyled v-if="errorLog">
|
||||||
|
<button @click="openInstallLog"><FileIcon />Open Installation Log</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="copyServerDebugInfo">
|
||||||
|
<CopyIcon v-if="!copied" />
|
||||||
|
<CheckIcon v-else />
|
||||||
|
Copy Debug Info
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="red" type="standard">
|
||||||
|
<NuxtLink
|
||||||
|
class="whitespace-pre"
|
||||||
|
:to="`/servers/manage/${serverId}/options/loader`"
|
||||||
|
>
|
||||||
|
<RightArrowIcon />
|
||||||
|
Change Loader
|
||||||
|
</NuxtLink>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!isConnected && !isReconnecting && !isLoading"
|
v-if="!isConnected && !isReconnecting && !isLoading"
|
||||||
data-pyro-server-ws-error
|
data-pyro-server-ws-error
|
||||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-red p-4 text-contrast"
|
||||||
>
|
>
|
||||||
<IssuesIcon class="size-5 text-red" />
|
<IssuesIcon class="size-5 text-red" />
|
||||||
Something went wrong...
|
Something went wrong...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isReconnecting"
|
v-if="isReconnecting"
|
||||||
data-pyro-server-ws-reconnecting
|
data-pyro-server-ws-reconnecting
|
||||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-orange p-4 text-sm text-contrast"
|
||||||
>
|
>
|
||||||
<UiServersPanelSpinner />
|
<UiServersPanelSpinner />
|
||||||
Hang on, we're reconnecting to your server.
|
Hang on, we're reconnecting to your server.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="serverData.status === 'installing'"
|
v-if="serverData.status === 'installing'"
|
||||||
data-pyro-server-installing
|
data-pyro-server-installing
|
||||||
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
class="mb-4 flex w-full flex-row items-center gap-4 rounded-2xl bg-bg-blue p-4 text-sm text-contrast"
|
||||||
>
|
>
|
||||||
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
<UiServersServerIcon :image="serverData.image" class="!h-10 !w-10" />
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<span class="text-lg font-bold"> We're preparing your server! </span>
|
<span class="text-lg font-bold"> We're preparing your server! </span>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
<UiServersPanelSpinner class="!h-3 !w-3" /> <LazyUiServersInstallingTicker />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<NuxtPage
|
||||||
|
:route="route"
|
||||||
|
:is-connected="isConnected"
|
||||||
|
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
||||||
|
:is-server-running="isServerRunning"
|
||||||
|
:stats="stats"
|
||||||
|
:server-power-state="serverPowerState"
|
||||||
|
:power-state-details="powerStateDetails"
|
||||||
|
:socket="socket"
|
||||||
|
:server="server"
|
||||||
|
:backup-in-progress="backupInProgress"
|
||||||
|
@reinstall="onReinstall"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<NuxtPage
|
</template>
|
||||||
:route="route"
|
</div>
|
||||||
:is-connected="isConnected"
|
<div
|
||||||
:is-ws-auth-incorrect="isWSAuthIncorrect"
|
v-if="flags.advancedDebugInfo"
|
||||||
:is-server-running="isServerRunning"
|
class="experimental-styles-within relative mx-auto mt-6 box-border w-full min-w-0 max-w-[1280px] px-6"
|
||||||
:stats="stats"
|
>
|
||||||
:server-power-state="serverPowerState"
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Server data</h2>
|
||||||
:power-state-details="powerStateDetails"
|
<pre class="markdown-body w-full overflow-auto rounded-2xl bg-bg-raised p-4 text-sm">{{
|
||||||
:socket="socket"
|
JSON.stringify(server, null, " ")
|
||||||
:server="server"
|
}}</pre>
|
||||||
:backup-in-progress="backupInProgress"
|
|
||||||
@reinstall="onReinstall"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
|
||||||
import {
|
import {
|
||||||
|
SettingsIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
LeftArrowIcon,
|
LeftArrowIcon,
|
||||||
@@ -384,14 +368,23 @@ import {
|
|||||||
LockIcon,
|
LockIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { ButtonStyled, ServerNotice } from "@modrinth/ui";
|
import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
|
||||||
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
||||||
import { reloadNuxtApp, navigateTo } from "#app";
|
|
||||||
import type { MessageDescriptor } from "@vintl/vintl";
|
import type { MessageDescriptor } from "@vintl/vintl";
|
||||||
import type { ServerState, Stats, WSEvent, WSInstallationResultEvent } from "~/types/servers";
|
import type {
|
||||||
import { usePyroConsole } from "~/store/console.ts";
|
ServerState,
|
||||||
import { type Backup } from "~/composables/pyroServers.ts";
|
Stats,
|
||||||
import { usePyroFetch } from "~/composables/pyroFetch.ts";
|
WSEvent,
|
||||||
|
WSInstallationResultEvent,
|
||||||
|
Backup,
|
||||||
|
PowerAction,
|
||||||
|
} from "@modrinth/utils";
|
||||||
|
import { reloadNuxtApp, navigateTo } from "#app";
|
||||||
|
import { useModrinthServersConsole } from "~/store/console.ts";
|
||||||
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
|
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||||
|
import ServerInstallation from "~/components/ui/servers/ServerInstallation.vue";
|
||||||
|
import PanelErrorIcon from "~/components/ui/servers/icons/PanelErrorIcon.vue";
|
||||||
|
|
||||||
const app = useNuxtApp() as unknown as { $notify: any };
|
const app = useNuxtApp() as unknown as { $notify: any };
|
||||||
|
|
||||||
@@ -401,6 +394,7 @@ const isLoading = ref(true);
|
|||||||
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
||||||
const isFirstMount = ref(true);
|
const isFirstMount = ref(true);
|
||||||
const isMounted = ref(true);
|
const isMounted = ref(true);
|
||||||
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
const INTERCOM_APP_ID = ref("ykeritl9");
|
const INTERCOM_APP_ID = ref("ykeritl9");
|
||||||
const auth = (await useAuth()) as unknown as {
|
const auth = (await useAuth()) as unknown as {
|
||||||
@@ -417,19 +411,19 @@ const route = useNativeRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const serverId = route.params.id as string;
|
const serverId = route.params.id as string;
|
||||||
|
|
||||||
const server = await usePyroServer(serverId, ["general", "ws"]);
|
const server: Reactive<ModrinthServer> = await useModrinthServers(serverId, ["general", "ws"]);
|
||||||
|
|
||||||
const loadModulesPromise = Promise.resolve().then(() => {
|
const loadModulesPromise = Promise.resolve().then(() => {
|
||||||
if (server.general?.status === "suspended") {
|
if (server.general?.status === "suspended") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return server.loadModules(["content", "backups", "network", "startup", "fs"]);
|
return server.refresh(["content", "backups", "network", "startup", "fs"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
provide("modulesLoaded", loadModulesPromise);
|
provide("modulesLoaded", loadModulesPromise);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [server.general?.error, server.ws?.error],
|
() => [server.moduleErrors?.general, server.moduleErrors?.ws],
|
||||||
([generalError, wsError]) => {
|
([generalError, wsError]) => {
|
||||||
if (server.general?.status === "suspended") return;
|
if (server.general?.status === "suspended") return;
|
||||||
|
|
||||||
@@ -447,7 +441,7 @@ const errorLogFile = ref("");
|
|||||||
const serverData = computed(() => server.general);
|
const serverData = computed(() => server.general);
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
const isWSAuthIncorrect = ref(false);
|
const isWSAuthIncorrect = ref(false);
|
||||||
const pyroConsole = usePyroConsole();
|
const modrinthServersConsole = useModrinthServersConsole();
|
||||||
const cpuData = ref<number[]>([]);
|
const cpuData = ref<number[]>([]);
|
||||||
const ramData = ref<number[]>([]);
|
const ramData = ref<number[]>([]);
|
||||||
const isActioning = ref(false);
|
const isActioning = ref(false);
|
||||||
@@ -621,7 +615,7 @@ const connectWebSocket = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pyroConsole.clear();
|
modrinthServersConsole.clear();
|
||||||
socket.value?.send(JSON.stringify({ event: "auth", jwt: wsAuth.value?.token }));
|
socket.value?.send(JSON.stringify({ event: "auth", jwt: wsAuth.value?.token }));
|
||||||
isConnected.value = true;
|
isConnected.value = true;
|
||||||
isReconnecting.value = false;
|
isReconnecting.value = false;
|
||||||
@@ -629,7 +623,7 @@ const connectWebSocket = () => {
|
|||||||
|
|
||||||
if (firstConnect.value) {
|
if (firstConnect.value) {
|
||||||
for (let i = 0; i < initialConsoleMessage.length; i++) {
|
for (let i = 0; i < initialConsoleMessage.length; i++) {
|
||||||
pyroConsole.addLine(initialConsoleMessage[i]);
|
modrinthServersConsole.addLine(initialConsoleMessage[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,7 +646,9 @@ const connectWebSocket = () => {
|
|||||||
|
|
||||||
socket.value.onclose = () => {
|
socket.value.onclose = () => {
|
||||||
if (isMounted.value) {
|
if (isMounted.value) {
|
||||||
pyroConsole.addLine("\nSomething went wrong with the connection, we're reconnecting...");
|
modrinthServersConsole.addLine(
|
||||||
|
"\nSomething went wrong with the connection, we're reconnecting...",
|
||||||
|
);
|
||||||
isConnected.value = false;
|
isConnected.value = false;
|
||||||
scheduleReconnect();
|
scheduleReconnect();
|
||||||
}
|
}
|
||||||
@@ -701,7 +697,7 @@ const startUptimeUpdates = () => {
|
|||||||
const stopUptimeUpdates = () => {
|
const stopUptimeUpdates = () => {
|
||||||
if (uptimeIntervalId) {
|
if (uptimeIntervalId) {
|
||||||
clearInterval(uptimeIntervalId);
|
clearInterval(uptimeIntervalId);
|
||||||
intervalId = null;
|
pollingIntervalId = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -710,7 +706,7 @@ const handleWebSocketMessage = (data: WSEvent) => {
|
|||||||
case "log":
|
case "log":
|
||||||
// eslint-disable-next-line no-case-declarations
|
// eslint-disable-next-line no-case-declarations
|
||||||
const log = data.message.split("\n").filter((l) => l.trim());
|
const log = data.message.split("\n").filter((l) => l.trim());
|
||||||
pyroConsole.addLines(log);
|
modrinthServersConsole.addLines(log);
|
||||||
break;
|
break;
|
||||||
case "stats":
|
case "stats":
|
||||||
updateStats(data);
|
updateStats(data);
|
||||||
@@ -812,6 +808,10 @@ const newLoaderVersion = ref<string | null>(null);
|
|||||||
const newMCVersion = ref<string | null>(null);
|
const newMCVersion = ref<string | null>(null);
|
||||||
|
|
||||||
const onReinstall = (potentialArgs: any) => {
|
const onReinstall = (potentialArgs: any) => {
|
||||||
|
if (serverData.value?.flows?.intro) {
|
||||||
|
server.general?.endIntro();
|
||||||
|
}
|
||||||
|
|
||||||
if (!serverData.value) return;
|
if (!serverData.value) return;
|
||||||
|
|
||||||
serverData.value.status = "installing";
|
serverData.value.status = "installing";
|
||||||
@@ -967,11 +967,11 @@ const toAdverb = (word: string) => {
|
|||||||
return word + "ing";
|
return word + "ing";
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendPowerAction = async (action: "restart" | "start" | "stop" | "kill") => {
|
const sendPowerAction = async (action: PowerAction) => {
|
||||||
const actionName = action.charAt(0).toUpperCase() + action.slice(1);
|
const actionName = action.charAt(0).toUpperCase() + action.slice(1);
|
||||||
try {
|
try {
|
||||||
isActioning.value = true;
|
isActioning.value = true;
|
||||||
await server.general?.power(actionName);
|
await server.general?.power(action);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error ${toAdverb(actionName)} server:`, error);
|
console.error(`Error ${toAdverb(actionName)} server:`, error);
|
||||||
notifyError(
|
notifyError(
|
||||||
@@ -992,7 +992,7 @@ const notifyError = (title: string, text: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
let pollingIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
const countdown = ref(15);
|
const countdown = ref(15);
|
||||||
|
|
||||||
const formattedTime = computed(() => {
|
const formattedTime = computed(() => {
|
||||||
@@ -1036,23 +1036,142 @@ const backupInProgress = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
if (intervalId) {
|
if (pollingIntervalId) {
|
||||||
clearInterval(intervalId);
|
clearTimeout(pollingIntervalId);
|
||||||
intervalId = null;
|
pollingIntervalId = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
countdown.value = 15;
|
stopPolling();
|
||||||
intervalId = setInterval(() => {
|
|
||||||
if (countdown.value <= 0) {
|
let retryCount = 0;
|
||||||
reloadNuxtApp();
|
const maxRetries = 10;
|
||||||
} else {
|
|
||||||
countdown.value--;
|
const poll = async () => {
|
||||||
|
try {
|
||||||
|
await server.refresh(["general", "ws"]);
|
||||||
|
|
||||||
|
if (!server.moduleErrors?.general?.error) {
|
||||||
|
stopPolling();
|
||||||
|
connectWebSocket();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount++;
|
||||||
|
if (retryCount >= maxRetries) {
|
||||||
|
console.error("Max retries reached, stopping polling");
|
||||||
|
stopPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponential backoff: 3s, 6s, 12s, 24s, etc.
|
||||||
|
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
|
||||||
|
|
||||||
|
pollingIntervalId = setTimeout(poll, delay);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Polling failed:", error);
|
||||||
|
retryCount++;
|
||||||
|
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
|
||||||
|
pollingIntervalId = setTimeout(poll, delay);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
};
|
||||||
|
|
||||||
|
poll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nodeUnavailableDetails = computed(() => [
|
||||||
|
{
|
||||||
|
label: "Server ID",
|
||||||
|
value: server.serverId,
|
||||||
|
type: "inline" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Node",
|
||||||
|
value: server.general?.datacenter ?? "Unknown! Please contact support!",
|
||||||
|
type: "inline" as const,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const suspendedDescription = computed(() => {
|
||||||
|
if (serverData.value?.suspension_reason === "cancelled") {
|
||||||
|
return "Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error.";
|
||||||
|
}
|
||||||
|
if (serverData.value?.suspension_reason) {
|
||||||
|
return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`;
|
||||||
|
}
|
||||||
|
return "Your server has been suspended.\nContact Modrinth Support if you believe this is an error.";
|
||||||
|
});
|
||||||
|
|
||||||
|
const generalErrorDetails = computed(() => [
|
||||||
|
{
|
||||||
|
label: "Server ID",
|
||||||
|
value: server.serverId,
|
||||||
|
type: "inline" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Timestamp",
|
||||||
|
value: String(server.moduleErrors?.general?.timestamp),
|
||||||
|
type: "inline" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Error Name",
|
||||||
|
value: server.moduleErrors?.general?.error.name,
|
||||||
|
type: "inline" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Error Message",
|
||||||
|
value: server.moduleErrors?.general?.error.message,
|
||||||
|
type: "block" as const,
|
||||||
|
},
|
||||||
|
...(server.moduleErrors?.general?.error.originalError
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: "Original Error",
|
||||||
|
value: String(server.moduleErrors.general.error.originalError),
|
||||||
|
type: "hidden" as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(server.moduleErrors?.general?.error.stack
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: "Stack Trace",
|
||||||
|
value: server.moduleErrors.general.error.stack,
|
||||||
|
type: "hidden" as const,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const suspendedAction = computed(() => ({
|
||||||
|
label: "Go to billing settings",
|
||||||
|
onClick: () => router.push("/settings/billing"),
|
||||||
|
color: "brand" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const generalErrorAction = computed(() => ({
|
||||||
|
label: "Go back to all servers",
|
||||||
|
onClick: () => router.push("/servers/manage"),
|
||||||
|
color: "brand" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const nodeUnavailableAction = computed(() => ({
|
||||||
|
label: "Join Modrinth Discord",
|
||||||
|
onClick: () => navigateTo("https://discord.modrinth.com", { external: true }),
|
||||||
|
color: "standard" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const connectionLostAction = computed(() => ({
|
||||||
|
label: "Reload",
|
||||||
|
onClick: () => reloadNuxtApp(),
|
||||||
|
color: "brand" as const,
|
||||||
|
disabled: formattedTime.value !== "00",
|
||||||
|
}));
|
||||||
|
|
||||||
const copyServerDebugInfo = () => {
|
const copyServerDebugInfo = () => {
|
||||||
const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`;
|
const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`;
|
||||||
navigator.clipboard.writeText(debugInfo);
|
navigator.clipboard.writeText(debugInfo);
|
||||||
@@ -1104,7 +1223,7 @@ const cleanup = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function dismissNotice(noticeId: number) {
|
async function dismissNotice(noticeId: number) {
|
||||||
await usePyroFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
|
await useServersFetch(`servers/${serverId}/notices/${noticeId}/dismiss`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
app.$notify({
|
app.$notify({
|
||||||
@@ -1123,14 +1242,18 @@ onMounted(() => {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (server.error) {
|
if (server.moduleErrors.general?.error) {
|
||||||
if (!server.error.message.includes("Forbidden")) {
|
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (server.general?.flows?.intro && server.general?.project) {
|
||||||
|
server.general?.endIntro();
|
||||||
|
}
|
||||||
|
|
||||||
if (username.value && email.value && userId.value && createdAt.value) {
|
if (username.value && email.value && userId.value && createdAt.value) {
|
||||||
const currentUser = auth.value?.user as any;
|
const currentUser = auth.value?.user as any;
|
||||||
const matches =
|
const matches =
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user