Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf309fd641 | ||
|
|
ced073d26c | ||
|
|
cc34e69524 | ||
|
|
d4864deac5 | ||
|
|
125207880d | ||
|
|
a8f17f40f5 | ||
|
|
dbde3c4669 | ||
|
|
ba4fecb0cb | ||
|
|
ef04dcc37b | ||
|
|
65126b3a23 | ||
|
|
58495e6276 | ||
|
|
706976439d | ||
|
|
0a9ffd3dc8 | ||
|
|
fb30c0ba2b | ||
|
|
97e4d8e132 | ||
|
|
5bdff3929b | ||
|
|
a08562bfe2 | ||
|
|
2b4319ea55 | ||
|
|
c32405720d | ||
|
|
b9ba3cd3e8 | ||
|
|
31381c860b | ||
|
|
e410a07cac | ||
|
|
9f93cd8705 | ||
|
|
dd391be095 | ||
|
|
f84f8c1c2b | ||
|
|
301967d204 | ||
|
|
c9b98a6154 | ||
|
|
ab8e474339 | ||
|
|
8a26011e76 | ||
|
|
d4de1dc9a1 | ||
|
|
4e3bd4e282 | ||
|
|
d24528f6a6 | ||
|
|
1b1d41605b | ||
|
|
6955731def | ||
|
|
4386891716 | ||
|
|
6741aba880 | ||
|
|
ee8ee7af82 | ||
|
|
a2e323c9ee | ||
|
|
f8fb23e05f |
No files matched your search
+2
-2
@@ -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"]
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/apps/frontend/ @modrinth/frontend
|
||||||
|
/apps/app-frontend/ @modrinth/frontend
|
||||||
|
/apps/daedalus_client/ @modrinth/backend
|
||||||
|
/apps/docs/ @modrinth/support @modrinth/platform @modrinth/servers
|
||||||
|
/apps/labrinth @modrinth/backend
|
||||||
|
/apps/app @modrinth/backend
|
||||||
|
|
||||||
|
/packages/app-lib/ @modrinth/backend
|
||||||
|
/packages/ariadne/ @modrinth/backend
|
||||||
|
/packages/assets/ @modrinth/frontend
|
||||||
|
/packages/daedalus/ @modrinth/backend
|
||||||
|
/packages/eslint-config-custom/ @modrinth/frontend
|
||||||
|
/packages/tsconfig/ @modrinth/frontend
|
||||||
|
/packages/ui/ @modrinth/frontend
|
||||||
|
/packages/utils/ @modrinth/frontend
|
||||||
|
|
||||||
|
README.md @modrinth/support
|
||||||
|
|
||||||
|
LICENSE @Geometrically @Prospector
|
||||||
|
/COPYING.md @Geometrically @Prospector
|
||||||
|
/apps/frontend/src/pages/legal/ @Geometrically @Prospector
|
||||||
|
|
||||||
|
/.github/ @Geometrically @Prospector
|
||||||
|
|
||||||
|
/docker-compose.yml @modrinth/backend
|
||||||
@@ -32,16 +32,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Rust setup (mac)
|
- name: Rust setup (mac)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
|
|
||||||
- name: Rust setup
|
- name: Rust setup
|
||||||
if: "!startsWith(matrix.platform, 'macos')"
|
if: "!startsWith(matrix.platform, 'macos')"
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
|
|
||||||
- name: Setup rust cache
|
- name: Setup rust cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -72,10 +72,10 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-rust-target-
|
${{ runner.os }}-rust-target-
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: .nvmrc
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: Install pnpm via corepack
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
merge_group:
|
merge_group:
|
||||||
@@ -10,71 +10,69 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build, Test, and Lint
|
name: Lint and Test
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Ensure pnpm output is colored in GitHub Actions logs
|
||||||
|
FORCE_COLOR: 3
|
||||||
|
# Make cargo nextest successfully ignore projects without tests
|
||||||
|
NEXTEST_NO_TESTS: pass
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: 📥 Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Cache turbo build setup
|
- name: 🧰 Install build dependencies
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: .turbo
|
|
||||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-turbo-
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||||
|
|
||||||
- name: Setup Node.JS environment
|
- name: 🧰 Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: 🧰 Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: .nvmrc
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: 🧰 Setup Rust toolchain
|
||||||
shell: bash
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
run: |
|
|
||||||
corepack enable
|
|
||||||
corepack prepare --activate
|
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
id: pnpm-cache
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
name: Setup pnpm cache
|
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
rustflags: ''
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
components: clippy, rustfmt
|
||||||
restore-keys: |
|
cache: false
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: 🧰 Setup nextest
|
||||||
|
uses: taiki-e/install-action@nextest
|
||||||
|
|
||||||
|
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||||
|
# back to a cached cargo install
|
||||||
|
- name: 🧰 Setup cargo-sqlx
|
||||||
|
uses: AlexTMjugador/cache-cargo-install-action@feat/features-support
|
||||||
|
with:
|
||||||
|
tool: sqlx-cli
|
||||||
|
locked: false
|
||||||
|
no-default-features: true
|
||||||
|
features: rustls,postgres
|
||||||
|
|
||||||
|
- name: 💨 Setup Turbo cache
|
||||||
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
|
|
||||||
|
- name: 🧰 Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build
|
- name: ⚙️ Start services
|
||||||
run: pnpm build
|
run: docker compose up --wait
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
|
|
||||||
- name: Lint
|
- name: ⚙️ Setup Labrinth environment and database
|
||||||
run: pnpm lint
|
working-directory: apps/labrinth
|
||||||
env:
|
run: |
|
||||||
SQLX_OFFLINE: true
|
cp .env.local .env
|
||||||
|
sqlx database setup
|
||||||
|
|
||||||
- name: Start docker compose
|
- name: 🔍 Lint and test
|
||||||
run: docker compose up -d
|
run: pnpm run ci
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: pnpm test
|
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
|
||||||
Generated
+6
@@ -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>
|
||||||
|
|||||||
Generated
+14
@@ -1379,6 +1379,17 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chardetng"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"encoding_rs",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.41"
|
version = "0.4.41"
|
||||||
@@ -8865,6 +8876,7 @@ dependencies = [
|
|||||||
"async_zip",
|
"async_zip",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"chardetng",
|
||||||
"chrono",
|
"chrono",
|
||||||
"daedalus",
|
"daedalus",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
@@ -8872,6 +8884,7 @@ dependencies = [
|
|||||||
"discord-rich-presence",
|
"discord-rich-presence",
|
||||||
"dunce",
|
"dunce",
|
||||||
"either",
|
"either",
|
||||||
|
"encoding_rs",
|
||||||
"enumset",
|
"enumset",
|
||||||
"flate2",
|
"flate2",
|
||||||
"fs4",
|
"fs4",
|
||||||
@@ -8890,6 +8903,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_ini",
|
"serde_ini",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_with",
|
||||||
"sha1_smol",
|
"sha1_smol",
|
||||||
"sha2",
|
"sha2",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|||||||
+51
-8
@@ -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,6 +24,7 @@ actix-web-prom = "0.10.0"
|
|||||||
actix-ws = "0.3.0"
|
actix-ws = "0.3.0"
|
||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
|
async_zip = "0.0.17"
|
||||||
async-compression = { version = "0.4.24", default-features = false }
|
async-compression = { version = "0.4.24", default-features = false }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
@@ -31,11 +35,11 @@ async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
|||||||
"futures-03-sink",
|
"futures-03-sink",
|
||||||
] }
|
] }
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
async_zip = "0.0.17"
|
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.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.40"
|
clap = "4.5.40"
|
||||||
clickhouse = "0.13.3"
|
clickhouse = "0.13.3"
|
||||||
@@ -50,6 +54,7 @@ dotenv-build = "0.1.1"
|
|||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
|
encoding_rs = "0.8.35"
|
||||||
enumset = "1.1.6"
|
enumset = "1.1.6"
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.2"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
@@ -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" # Locked on 0.31 until deadpool-redis updates to 0.32
|
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.19", 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,12 +118,12 @@ 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.1" # 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"
|
||||||
@@ -168,6 +173,44 @@ zip = { version = "4.0.0", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
zxcvbn = "3.1.0"
|
zxcvbn = "3.1.0"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
bool_to_int_with_if = "warn"
|
||||||
|
borrow_as_ptr = "warn"
|
||||||
|
cfg_not_test = "warn"
|
||||||
|
clear_with_drain = "warn"
|
||||||
|
cloned_instead_of_copied = "warn"
|
||||||
|
collection_is_never_read = "warn"
|
||||||
|
dbg_macro = "warn"
|
||||||
|
default_trait_access = "warn"
|
||||||
|
explicit_iter_loop = "warn"
|
||||||
|
filter_map_next = "warn"
|
||||||
|
flat_map_option = "warn"
|
||||||
|
format_push_string = "warn"
|
||||||
|
get_unwrap = "warn"
|
||||||
|
large_include_file = "warn"
|
||||||
|
large_stack_arrays = "warn"
|
||||||
|
manual_assert = "warn"
|
||||||
|
manual_instant_elapsed = "warn"
|
||||||
|
manual_is_variant_and = "warn"
|
||||||
|
manual_let_else = "warn"
|
||||||
|
map_unwrap_or = "warn"
|
||||||
|
match_bool = "warn"
|
||||||
|
needless_collect = "warn"
|
||||||
|
negative_feature_names = "warn"
|
||||||
|
non_std_lazy_statics = "warn"
|
||||||
|
pathbuf_init_then_push = "warn"
|
||||||
|
read_zero_byte_vec = "warn"
|
||||||
|
redundant_clone = "warn"
|
||||||
|
redundant_feature_names = "warn"
|
||||||
|
redundant_type_annotations = "warn"
|
||||||
|
todo = "warn"
|
||||||
|
unnested_or_patterns = "warn"
|
||||||
|
wildcard_dependencies = "warn"
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
# Turn warnings into errors by default
|
||||||
|
warnings = "deny"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
||||||
|
|
||||||
|
|||||||
@@ -9,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,11 +1,7 @@
|
|||||||
<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,
|
|
||||||
getWorldIdentifier,
|
|
||||||
showWorldInFolder,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
@@ -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(
|
||||||
@@ -380,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,
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-3
@@ -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"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub fn get_os() -> OS {
|
|||||||
let os = OS::MacOS;
|
let os = OS::MacOS;
|
||||||
os
|
os
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum OS {
|
pub enum OS {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ pub async fn get_recent_worlds<R: Runtime>(
|
|||||||
display_statuses.unwrap_or(EnumSet::all()),
|
display_statuses.unwrap_or(EnumSet::all()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
for world in result.iter_mut() {
|
for world in &mut result {
|
||||||
adapt_world_icon(&app_handle, &mut world.world);
|
adapt_world_icon(&app_handle, &mut world.world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -55,7 +55,7 @@ pub async fn get_profile_worlds<R: Runtime>(
|
|||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<Vec<World>> {
|
) -> Result<Vec<World>> {
|
||||||
let mut result = worlds::get_profile_worlds(path).await?;
|
let mut result = worlds::get_profile_worlds(path).await?;
|
||||||
for world in result.iter_mut() {
|
for world in &mut result {
|
||||||
adapt_world_icon(&app_handle, world);
|
adapt_world_icon(&app_handle, world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager: &M,
|
manager: &M,
|
||||||
) -> InitialPayload {
|
) -> InitialPayload {
|
||||||
let initial_payload = manager.try_state::<InitialPayload>();
|
let initial_payload = manager.try_state::<InitialPayload>();
|
||||||
let mtx = if let Some(initial_payload) = initial_payload {
|
|
||||||
|
if let Some(initial_payload) = initial_payload {
|
||||||
initial_payload.inner().clone()
|
initial_payload.inner().clone()
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("No initial payload found, creating new");
|
tracing::info!("No initial payload found, creating new");
|
||||||
@@ -22,7 +23,5 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager.manage(payload.clone());
|
manager.manage(payload.clone());
|
||||||
|
|
||||||
payload
|
payload
|
||||||
};
|
}
|
||||||
|
|
||||||
mtx
|
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ fn main() {
|
|||||||
{
|
{
|
||||||
let payload = macos::deep_link::get_or_init_payload(app);
|
let payload = macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload.clone();
|
let mtx_copy = payload.payload;
|
||||||
app.listen("deep-link://new-url", move |url| {
|
app.listen("deep-link://new-url", move |url| {
|
||||||
let mtx_copy_copy = mtx_copy.clone();
|
let mtx_copy_copy = mtx_copy.clone();
|
||||||
let request = url.payload().to_owned();
|
let request = url.payload().to_owned();
|
||||||
@@ -229,7 +229,6 @@ fn main() {
|
|||||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||||
payload,
|
payload,
|
||||||
));
|
));
|
||||||
dbg!(url);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
@@ -273,22 +272,22 @@ fn main() {
|
|||||||
|
|
||||||
match app {
|
match app {
|
||||||
Ok(app) => {
|
Ok(app) => {
|
||||||
#[allow(unused_variables)]
|
|
||||||
app.run(|app, event| {
|
app.run(|app, event| {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
drop((app, event));
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let tauri::RunEvent::Opened { urls } = event {
|
if let tauri::RunEvent::Opened { urls } = event {
|
||||||
tracing::info!("Handling webview open {urls:?}");
|
tracing::info!("Handling webview open {urls:?}");
|
||||||
|
|
||||||
let file = urls
|
let file = urls
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|url| url.to_file_path().ok())
|
.find_map(|url| url.to_file_path().ok());
|
||||||
.next();
|
|
||||||
|
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
let payload =
|
let payload =
|
||||||
macos::deep_link::get_or_init_payload(app);
|
macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload.clone();
|
let mtx_copy = payload.payload;
|
||||||
let request = file.to_string_lossy().to_string();
|
let request = file.to_string_lossy().to_string();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut payload = mtx_copy.lock().await;
|
let mut payload = mtx_copy.lock().await;
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,11 +85,10 @@ During development, you might notice that changes made directly to entities in t
|
|||||||
|
|
||||||
#### CDN options
|
#### CDN options
|
||||||
|
|
||||||
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local`, `backblaze`, or `s3`, but defaults to `local`
|
`STORAGE_BACKEND`: Controls what storage backend is used. This can be either `local` or `s3`, but defaults to `local`
|
||||||
|
|
||||||
The Backblaze and S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
The S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
||||||
`BACKBLAZE_KEY_ID`, `BACKBLAZE_KEY`, `BACKBLAZE_BUCKET_ID`
|
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_PUBLIC_BUCKET_NAME`, `S3_PRIVATE_BUCKET_NAME`, `S3_USES_PATH_STYLE_BUCKETS`
|
||||||
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_BUCKET_NAME`
|
|
||||||
|
|
||||||
#### Search, OAuth, and miscellaneous options
|
#### Search, OAuth, and miscellaneous options
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ BASE_URL=http://127.0.0.1:8000/v2/
|
|||||||
BROWSER_BASE_URL=http://127.0.0.1:8000/v2/
|
BROWSER_BASE_URL=http://127.0.0.1:8000/v2/
|
||||||
PYRO_BASE_URL=https://staging-archon.modrinth.com
|
PYRO_BASE_URL=https://staging-archon.modrinth.com
|
||||||
PROD_OVERRIDE=true
|
PROD_OVERRIDE=true
|
||||||
|
|
||||||
@@ -10,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();
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="vue-notification-group experimental-styles-within">
|
<div
|
||||||
|
class="vue-notification-group experimental-styles-within"
|
||||||
|
:class="{ 'intercom-present': isIntercomPresent }"
|
||||||
|
>
|
||||||
<transition-group name="notifs">
|
<transition-group name="notifs">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in notifications"
|
v-for="(item, index) in notifications"
|
||||||
@@ -80,6 +83,8 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
const notifications = useNotifications();
|
const notifications = useNotifications();
|
||||||
|
|
||||||
|
const isIntercomPresent = ref(false);
|
||||||
|
|
||||||
function stopTimer(notif) {
|
function stopTimer(notif) {
|
||||||
clearTimeout(notif.timer);
|
clearTimeout(notif.timer);
|
||||||
}
|
}
|
||||||
@@ -106,6 +111,27 @@ const createNotifText = (notif) => {
|
|||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function checkIntercomPresence() {
|
||||||
|
isIntercomPresent.value = !!document.querySelector(".intercom-lightweight-app");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
checkIntercomPresence();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
checkIntercomPresence();
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function copyToClipboard(notif) {
|
function copyToClipboard(notif) {
|
||||||
const text = createNotifText(notif);
|
const text = createNotifText(notif);
|
||||||
|
|
||||||
@@ -130,6 +156,10 @@ function copyToClipboard(notif) {
|
|||||||
bottom: 0.75rem;
|
bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.intercom-present {
|
||||||
|
bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.vue-notification-wrapper {
|
.vue-notification-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<div class="table-cell">
|
<div class="table-cell">
|
||||||
<BoxIcon />
|
<BoxIcon />
|
||||||
<span>{{
|
<span>{{
|
||||||
$formatProjectType(
|
formatProjectType(
|
||||||
$getProjectTypeForDisplay(
|
$getProjectTypeForDisplay(
|
||||||
project.project_types?.[0] ?? "project",
|
project.project_types?.[0] ?? "project",
|
||||||
project.loaders,
|
project.loaders,
|
||||||
@@ -111,6 +111,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from "@modrinth/assets";
|
||||||
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
|
import { Button, Modal, Checkbox, CopyCode, Avatar } from "@modrinth/ui";
|
||||||
|
import { formatProjectType } from "@modrinth/utils";
|
||||||
|
|
||||||
const modalOpen = ref(null);
|
const modalOpen = ref(null);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ import {
|
|||||||
ScaleIcon,
|
ScaleIcon,
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { formatProjectType } from "~/plugins/shorthands.js";
|
import { formatProjectType } from "@modrinth/utils";
|
||||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
<div class="stacked">
|
<div class="stacked">
|
||||||
<span class="title">{{ report.project.title }}</span>
|
<span class="title">{{ report.project.title }}</span>
|
||||||
<span>{{
|
<span>{{
|
||||||
$formatProjectType(
|
formatProjectType(
|
||||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
<div class="stacked">
|
<div class="stacked">
|
||||||
<span class="title">{{ report.project.title }}</span>
|
<span class="title">{{ report.project.title }}</span>
|
||||||
<span>{{
|
<span>{{
|
||||||
$formatProjectType(
|
formatProjectType(
|
||||||
$getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
getProjectTypeForUrl(report.project.project_type, report.project.loaders),
|
||||||
)
|
)
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,12 +104,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
import { ReportIcon, UnknownIcon, VersionIcon } from "@modrinth/assets";
|
||||||
|
import { Avatar, Badge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
||||||
|
import { formatProjectType } from "@modrinth/utils";
|
||||||
import { renderHighlightedString } from "~/helpers/highlight.js";
|
import { renderHighlightedString } from "~/helpers/highlight.js";
|
||||||
import { useRelativeTime } from "@modrinth/ui";
|
|
||||||
import Avatar from "~/components/ui/Avatar.vue";
|
|
||||||
import Badge from "~/components/ui/Badge.vue";
|
|
||||||
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||||
import CopyCode from "~/components/ui/CopyCode.vue";
|
import { getProjectTypeForUrl } from "~/helpers/projects.js";
|
||||||
|
|
||||||
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) => {
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
<span
|
<span
|
||||||
v-for="category in categoriesFiltered"
|
v-for="category in categoriesFiltered"
|
||||||
:key="category.name"
|
:key="category.name"
|
||||||
v-html="category.icon + $formatCategory(category.name)"
|
v-html="category.icon + formatCategory(category.name)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { formatCategory } from "@modrinth/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
categories: {
|
categories: {
|
||||||
@@ -38,6 +40,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: { formatCategory },
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from "@modrinth/ui";
|
||||||
import { defineMessages, useVIntl } from "@vintl/vintl";
|
import { defineMessages, useVIntl } from "@vintl/vintl";
|
||||||
import { ref } from "vue";
|
import { ref, computed } 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();
|
||||||
@@ -52,9 +52,10 @@ const failedToCreate = computed(() => props.backup.interrupted);
|
|||||||
const preparedDownloadStates = ["ready", "done"];
|
const preparedDownloadStates = ["ready", "done"];
|
||||||
const inactiveStates = ["failed", "cancelled"];
|
const inactiveStates = ["failed", "cancelled"];
|
||||||
|
|
||||||
const hasPreparedDownload = computed(() =>
|
const hasPreparedDownload = computed(() => {
|
||||||
preparedDownloadStates.includes(props.backup.task?.file?.state ?? ""),
|
const fileState = props.backup.task?.file?.state ?? "";
|
||||||
);
|
return preparedDownloadStates.includes(fileState);
|
||||||
|
});
|
||||||
|
|
||||||
const creating = computed(() => {
|
const creating = computed(() => {
|
||||||
const task = props.backup.task?.create;
|
const task = props.backup.task?.create;
|
||||||
@@ -81,6 +82,10 @@ const restoring = computed(() => {
|
|||||||
const initiatedPrepare = ref(false);
|
const initiatedPrepare = ref(false);
|
||||||
|
|
||||||
const preparingFile = computed(() => {
|
const preparingFile = computed(() => {
|
||||||
|
if (hasPreparedDownload.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const task = props.backup.task?.file;
|
const task = props.backup.task?.file;
|
||||||
return (
|
return (
|
||||||
(!task && initiatedPrepare.value) ||
|
(!task && initiatedPrepare.value) ||
|
||||||
|
|||||||
@@ -48,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, {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
/>
|
/>
|
||||||
<XCircleIcon
|
<XCircleIcon
|
||||||
v-show="
|
v-show="
|
||||||
item.status === 'error' ||
|
item.status.includes('error') ||
|
||||||
item.status === 'cancelled' ||
|
item.status === 'cancelled' ||
|
||||||
item.status === 'incorrect-type'
|
item.status === 'incorrect-type'
|
||||||
"
|
"
|
||||||
@@ -54,9 +54,14 @@
|
|||||||
<template v-if="item.status === 'completed'">
|
<template v-if="item.status === 'completed'">
|
||||||
<span>Done</span>
|
<span>Done</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="item.status === 'error'">
|
<template v-else-if="item.status === 'error-file-exists'">
|
||||||
<span class="text-red">Failed - File already exists</span>
|
<span class="text-red">Failed - File already exists</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="item.status === 'error-generic'">
|
||||||
|
<span class="text-red"
|
||||||
|
>Failed - {{ item.error?.message || "An unexpected error occured." }}</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
<template v-else-if="item.status === 'incorrect-type'">
|
<template v-else-if="item.status === 'incorrect-type'">
|
||||||
<span class="text-red">Failed - Incorrect file type</span>
|
<span class="text-red">Failed - Incorrect file type</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -99,14 +104,22 @@
|
|||||||
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;
|
||||||
progress: number;
|
progress: number;
|
||||||
status: "pending" | "uploading" | "completed" | "error" | "cancelled" | "incorrect-type";
|
status:
|
||||||
|
| "pending"
|
||||||
|
| "uploading"
|
||||||
|
| "completed"
|
||||||
|
| "error-file-exists"
|
||||||
|
| "error-generic"
|
||||||
|
| "cancelled"
|
||||||
|
| "incorrect-type";
|
||||||
size: string;
|
size: string;
|
||||||
uploader?: any;
|
uploader?: any;
|
||||||
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -245,8 +258,18 @@ const uploadFile = async (file: File) => {
|
|||||||
console.error("Error uploading file:", error);
|
console.error("Error uploading file:", error);
|
||||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name);
|
||||||
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
if (index !== -1 && uploadQueue.value[index].status !== "cancelled") {
|
||||||
uploadQueue.value[index].status =
|
const target = uploadQueue.value[index];
|
||||||
error instanceof Error && error.message === badFileTypeMsg ? "incorrect-type" : "error";
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === badFileTypeMsg) {
|
||||||
|
target.status = "incorrect-type";
|
||||||
|
} else if (target.progress === 100 && error.message.includes("401")) {
|
||||||
|
target.status = "error-file-exists";
|
||||||
|
} else {
|
||||||
|
target.status = "error-generic";
|
||||||
|
target.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|||||||
@@ -75,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.",
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -200,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();
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ 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;
|
initialSetup?: boolean;
|
||||||
@@ -458,7 +458,6 @@ 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,
|
||||||
@@ -474,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 () => {
|
||||||
|
|||||||
@@ -160,14 +160,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
import { ButtonStyled, NewProjectCard } from "@modrinth/ui";
|
||||||
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
import { TransferIcon, UploadIcon, InfoIcon, CompassIcon, SettingsIcon } from "@modrinth/assets";
|
||||||
import type { Server } from "~/composables/pyroServers";
|
import type { Loaders } 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();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
server: Server<["general", "content", "backups", "network", "startup", "ws", "fs"]>;
|
server: ModrinthServer;
|
||||||
ignoreCurrentInstallation?: boolean;
|
ignoreCurrentInstallation?: boolean;
|
||||||
backupInProgress?: BackupInProgressReason;
|
backupInProgress?: BackupInProgressReason;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@@ -81,12 +81,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, LockIcon, SparklesIcon } 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);
|
||||||
@@ -109,7 +110,7 @@ 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);
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
import { ButtonStyled, ServersSpecs } from "@modrinth/ui";
|
||||||
import type { MessageDescriptor } from "@vintl/vintl";
|
import type { MessageDescriptor } from "@vintl/vintl";
|
||||||
import { formatPrice } from "../../../../../../../packages/utils";
|
import { formatPrice } from "@modrinth/utils";
|
||||||
|
|
||||||
const { formatMessage, locale } = useVIntl();
|
const { formatMessage, locale } = useVIntl();
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,112 +0,0 @@
|
|||||||
import { $fetch, FetchError } from "ofetch";
|
|
||||||
|
|
||||||
interface PyroFetchOptions {
|
|
||||||
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
||||||
contentType?: string;
|
|
||||||
body?: Record<string, any>;
|
|
||||||
version?: number;
|
|
||||||
override?: {
|
|
||||||
url?: string;
|
|
||||||
token?: string;
|
|
||||||
};
|
|
||||||
retry?: boolean;
|
|
||||||
bypassAuth?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PyroFetchError extends Error {
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
public statusCode?: number,
|
|
||||||
public originalError?: Error,
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = "PyroFetchError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function usePyroFetch<T>(path: string, options: PyroFetchOptions = {}): Promise<T> {
|
|
||||||
const config = useRuntimeConfig();
|
|
||||||
const auth = await useAuth();
|
|
||||||
const authToken = auth.value?.token;
|
|
||||||
|
|
||||||
if (!authToken && !options.bypassAuth) {
|
|
||||||
throw new PyroFetchError("Cannot pyrofetch without auth", 10000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { method = "GET", contentType = "application/json", body, version = 0, override } = options;
|
|
||||||
|
|
||||||
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
|
||||||
/\/$/,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!base) {
|
|
||||||
throw new PyroFetchError(
|
|
||||||
"Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
|
|
||||||
10001,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullUrl = override?.url
|
|
||||||
? `https://${override.url}/${path.replace(/^\//, "")}`
|
|
||||||
: version === 0
|
|
||||||
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
|
|
||||||
: `${base}/v${version}/${path.replace(/^\//, "")}`;
|
|
||||||
|
|
||||||
type HeadersRecord = Record<string, string>;
|
|
||||||
|
|
||||||
const authHeader: HeadersRecord = options.bypassAuth
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
Authorization: `Bearer ${override?.token ?? authToken}`,
|
|
||||||
"Access-Control-Allow-Headers": "Authorization",
|
|
||||||
};
|
|
||||||
|
|
||||||
const headers: HeadersRecord = {
|
|
||||||
...authHeader,
|
|
||||||
"User-Agent": "Pyro/1.0 (https://pyro.host)",
|
|
||||||
Vary: "Accept, Origin",
|
|
||||||
"Content-Type": contentType,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (import.meta.client && typeof window !== "undefined") {
|
|
||||||
headers.Origin = window.location.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await $fetch<T>(fullUrl, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
|
||||||
timeout: 10000,
|
|
||||||
retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0,
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Fetch error:", error);
|
|
||||||
if (error instanceof FetchError) {
|
|
||||||
const statusCode = error.response?.status;
|
|
||||||
const statusText = error.response?.statusText || "Unknown error";
|
|
||||||
const errorMessages: { [key: number]: string } = {
|
|
||||||
400: "Bad Request",
|
|
||||||
401: "Unauthorized",
|
|
||||||
403: "Forbidden",
|
|
||||||
404: "Not Found",
|
|
||||||
405: "Method Not Allowed",
|
|
||||||
429: "Too Many Requests",
|
|
||||||
500: "Internal Server Error",
|
|
||||||
502: "Bad Gateway",
|
|
||||||
};
|
|
||||||
const message =
|
|
||||||
statusCode && statusCode in errorMessages
|
|
||||||
? errorMessages[statusCode]
|
|
||||||
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
|
||||||
throw new PyroFetchError(`[PYROFETCH][PYRO] ${message}`, statusCode, error);
|
|
||||||
}
|
|
||||||
throw new PyroFetchError(
|
|
||||||
"[PYROFETCH][PYRO] An unexpected error occurred during the fetch operation.",
|
|
||||||
undefined,
|
|
||||||
error as Error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||||
|
};
|
||||||
@@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { $fetch, FetchError } from "ofetch";
|
||||||
|
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
|
||||||
|
import type { V1ErrorInfo } from "@modrinth/utils";
|
||||||
|
|
||||||
|
export interface ServersFetchOptions {
|
||||||
|
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
|
contentType?: string;
|
||||||
|
body?: Record<string, any>;
|
||||||
|
version?: number;
|
||||||
|
override?: {
|
||||||
|
url?: string;
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
retry?: number | boolean;
|
||||||
|
bypassAuth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function useServersFetch<T>(
|
||||||
|
path: string,
|
||||||
|
options: ServersFetchOptions = {},
|
||||||
|
module?: string,
|
||||||
|
errorContext?: string,
|
||||||
|
): Promise<T> {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const auth = await useAuth();
|
||||||
|
const authToken = auth.value?.token;
|
||||||
|
|
||||||
|
if (!authToken && !options.bypassAuth) {
|
||||||
|
const error = new ModrinthServersFetchError(
|
||||||
|
"[Modrinth Servers] Cannot fetch without auth",
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Missing auth token", 401, error, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
method = "GET",
|
||||||
|
contentType = "application/json",
|
||||||
|
body,
|
||||||
|
version = 0,
|
||||||
|
override,
|
||||||
|
retry = method === "GET" ? 3 : 0,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
||||||
|
/\/$/,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!base) {
|
||||||
|
const error = new ModrinthServersFetchError(
|
||||||
|
"[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
|
||||||
|
10001,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Configuration error: Missing PYRO_BASE_URL", 500, error, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionString = `v${version}`;
|
||||||
|
let newOverrideUrl = override?.url;
|
||||||
|
if (newOverrideUrl && newOverrideUrl.includes("v0") && version !== 0) {
|
||||||
|
newOverrideUrl = newOverrideUrl.replace("v0", versionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = newOverrideUrl
|
||||||
|
? `https://${newOverrideUrl}/${path.replace(/^\//, "")}`
|
||||||
|
: version === 0
|
||||||
|
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
|
||||||
|
: `${base}/v${version}/${path.replace(/^\//, "")}`;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
||||||
|
Vary: "Accept, Origin",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.bypassAuth) {
|
||||||
|
headers.Authorization = `Bearer ${override?.token ?? authToken}`;
|
||||||
|
headers["Access-Control-Allow-Headers"] = "Authorization";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType !== "none") {
|
||||||
|
headers["Content-Type"] = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client && typeof window !== "undefined") {
|
||||||
|
headers.Origin = window.location.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = (typeof retry === "boolean" ? (retry ? 3 : 1) : retry) + 1;
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
try {
|
||||||
|
const response = await $fetch<T>(fullUrl, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error as Error;
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (error instanceof FetchError) {
|
||||||
|
const statusCode = error.response?.status;
|
||||||
|
const statusText = error.response?.statusText || "Unknown error";
|
||||||
|
|
||||||
|
let v1Error: V1ErrorInfo | undefined;
|
||||||
|
if (error.data?.error && error.data?.description) {
|
||||||
|
v1Error = {
|
||||||
|
context: errorContext,
|
||||||
|
...error.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessages: { [key: number]: string } = {
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Not Found",
|
||||||
|
405: "Method Not Allowed",
|
||||||
|
408: "Request Timeout",
|
||||||
|
429: "Too Many Requests",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
502: "Bad Gateway",
|
||||||
|
503: "Service Unavailable",
|
||||||
|
504: "Gateway Timeout",
|
||||||
|
};
|
||||||
|
|
||||||
|
const message =
|
||||||
|
statusCode && statusCode in errorMessages
|
||||||
|
? errorMessages[statusCode]
|
||||||
|
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
||||||
|
|
||||||
|
const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true;
|
||||||
|
|
||||||
|
if (!isRetryable || attempts >= maxAttempts) {
|
||||||
|
console.error("Fetch error:", error);
|
||||||
|
|
||||||
|
const fetchError = new ModrinthServersFetchError(
|
||||||
|
`[Modrinth Servers] ${message}`,
|
||||||
|
statusCode,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
|
||||||
|
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Unexpected fetch error:", error);
|
||||||
|
const fetchError = new ModrinthServersFetchError(
|
||||||
|
"[Modrinth Servers] An unexpected error occurred during the fetch operation.",
|
||||||
|
undefined,
|
||||||
|
error as Error,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError(
|
||||||
|
"Unexpected error during fetch operation",
|
||||||
|
undefined,
|
||||||
|
fetchError,
|
||||||
|
module,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("All retry attempts failed:", lastError);
|
||||||
|
if (lastError instanceof FetchError) {
|
||||||
|
const statusCode = lastError.response?.status;
|
||||||
|
const pyroError = new ModrinthServersFetchError(
|
||||||
|
"Maximum retry attempts reached",
|
||||||
|
statusCode,
|
||||||
|
lastError,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Maximum retry attempts reached", statusCode, pyroError, module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchError = new ModrinthServersFetchError(
|
||||||
|
"Maximum retry attempts reached",
|
||||||
|
undefined,
|
||||||
|
lastError || undefined,
|
||||||
|
);
|
||||||
|
throw new ModrinthServerError("Maximum retry attempts reached", undefined, fetchError, module);
|
||||||
|
}
|
||||||
@@ -77,9 +77,6 @@ const errorMessages = computed(
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
// TODO: REMOVE BEFORE MERGE
|
|
||||||
console.log(props.error);
|
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
console.log(route);
|
console.log(route);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { formatBytes } from "~/plugins/shorthands.js";
|
import { formatBytes } from "@modrinth/utils";
|
||||||
|
|
||||||
export const fileIsValid = (file, validationOptions) => {
|
export const fileIsValid = (file, validationOptions) => {
|
||||||
const { maxSize, alertOnInvalid } = validationOptions;
|
const { maxSize, alertOnInvalid } = validationOptions;
|
||||||
|
|||||||
@@ -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,17 +870,24 @@ import {
|
|||||||
ProjectSidebarCreators,
|
ProjectSidebarCreators,
|
||||||
ProjectSidebarDetails,
|
ProjectSidebarDetails,
|
||||||
ProjectSidebarLinks,
|
ProjectSidebarLinks,
|
||||||
|
ProjectStatusBadge,
|
||||||
ScrollablePanel,
|
ScrollablePanel,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
|
||||||
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
|
import {
|
||||||
|
formatCategory,
|
||||||
|
formatProjectType,
|
||||||
|
isRejected,
|
||||||
|
isStaff,
|
||||||
|
isUnderReview,
|
||||||
|
renderString,
|
||||||
|
} from "@modrinth/utils";
|
||||||
import { navigateTo } from "#app";
|
import { navigateTo } from "#app";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import 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";
|
||||||
@@ -1286,7 +1293,7 @@ featuredVersions.value.sort((a, b) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const projectTypeDisplay = computed(() =>
|
const projectTypeDisplay = computed(() =>
|
||||||
data.$formatProjectType(
|
formatProjectType(
|
||||||
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
|
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<span class="label__title">Client-side</span>
|
<span class="label__title">Client-side</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Select based on if the
|
Select based on if the
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
{{ formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||||
client side. Just because a mod works in Singleplayer doesn't mean it has actual
|
client side. Just because a mod works in Singleplayer doesn't mean it has actual
|
||||||
client-side functionality.
|
client-side functionality.
|
||||||
</span>
|
</span>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<span class="label__title">Server-side</span>
|
<span class="label__title">Server-side</span>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
Select based on if the
|
Select based on if the
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
{{ formatProjectType(project.project_type).toLowerCase() }} has functionality on the
|
||||||
<strong>logical</strong> server. Remember that Singleplayer contains an integrated
|
<strong>logical</strong> server. Remember that Singleplayer contains an integrated
|
||||||
server.
|
server.
|
||||||
</span>
|
</span>
|
||||||
@@ -239,11 +239,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { formatProjectStatus } from "@modrinth/utils";
|
import { formatProjectStatus, formatProjectType } 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({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Accurate tagging is important to help people find your
|
Accurate tagging is important to help people find your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
||||||
that apply.
|
that apply.
|
||||||
</p>
|
</p>
|
||||||
<p v-if="project.versions.length === 0" class="known-errors">
|
<p v-if="project.versions.length === 0" class="known-errors">
|
||||||
@@ -18,25 +18,25 @@
|
|||||||
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
|
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<h4>
|
<h4>
|
||||||
<span class="label__title">{{ $formatCategoryHeader(header) }}</span>
|
<span class="label__title">{{ formatCategoryHeader(header) }}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<span class="label__description">
|
<span class="label__description">
|
||||||
<template v-if="header === 'categories'">
|
<template v-if="header === 'categories'">
|
||||||
Select all categories that reflect the themes or function of your
|
Select all categories that reflect the themes or function of your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
{{ formatProjectType(project.project_type).toLowerCase() }}.
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="header === 'features'">
|
<template v-else-if="header === 'features'">
|
||||||
Select all of the features that your
|
Select all of the features that your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} makes use of.
|
{{ formatProjectType(project.project_type).toLowerCase() }} makes use of.
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="header === 'resolutions'">
|
<template v-else-if="header === 'resolutions'">
|
||||||
Select the resolution(s) of textures in your
|
Select the resolution(s) of textures in your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }}.
|
{{ formatProjectType(project.project_type).toLowerCase() }}.
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="header === 'performance impact'">
|
<template v-else-if="header === 'performance impact'">
|
||||||
Select the realistic performance impact of your
|
Select the realistic performance impact of your
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
|
{{ formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
|
||||||
{{ $formatProjectType(project.project_type).toLowerCase() }} is configurable to
|
{{ formatProjectType(project.project_type).toLowerCase() }} is configurable to
|
||||||
different levels of performance impact.
|
different levels of performance impact.
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
v-for="category in categoryLists[header]"
|
v-for="category in categoryLists[header]"
|
||||||
:key="`category-${header}-${category.name}`"
|
:key="`category-${header}-${category.name}`"
|
||||||
:model-value="selectedTags.includes(category)"
|
:model-value="selectedTags.includes(category)"
|
||||||
:description="$formatCategory(category.name)"
|
:description="formatCategory(category.name)"
|
||||||
class="category-selector"
|
class="category-selector"
|
||||||
@update:model-value="toggleCategory(category)"
|
@update:model-value="toggleCategory(category)"
|
||||||
>
|
>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
class="icon"
|
class="icon"
|
||||||
v-html="category.icon"
|
v-html="category.icon"
|
||||||
/>
|
/>
|
||||||
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
<span aria-hidden="true"> {{ formatCategory(category.name) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
:key="`featured-category-${category.name}`"
|
:key="`featured-category-${category.name}`"
|
||||||
class="category-selector"
|
class="category-selector"
|
||||||
:model-value="featuredTags.includes(category)"
|
:model-value="featuredTags.includes(category)"
|
||||||
:description="$formatCategory(category.name)"
|
:description="formatCategory(category.name)"
|
||||||
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
|
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
|
||||||
@update:model-value="toggleFeaturedCategory(category)"
|
@update:model-value="toggleFeaturedCategory(category)"
|
||||||
>
|
>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
class="icon"
|
class="icon"
|
||||||
v-html="category.icon"
|
v-html="category.icon"
|
||||||
/>
|
/>
|
||||||
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
|
<span aria-hidden="true"> {{ formatCategory(category.name) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { StarIcon, SaveIcon } from "@modrinth/assets";
|
import { StarIcon, SaveIcon } from "@modrinth/assets";
|
||||||
|
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils";
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
@@ -222,6 +223,9 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatProjectType,
|
||||||
|
formatCategoryHeader,
|
||||||
|
formatCategory,
|
||||||
toggleCategory(category) {
|
toggleCategory(category) {
|
||||||
if (this.selectedTags.includes(category)) {
|
if (this.selectedTags.includes(category)) {
|
||||||
this.selectedTags = this.selectedTags.filter((x) => x !== category);
|
this.selectedTags = this.selectedTags.filter((x) => x !== category);
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
<div v-else class="input-group">
|
<div v-else class="input-group">
|
||||||
<ButtonStyled v-if="primaryFile" color="brand">
|
<ButtonStyled v-if="primaryFile" color="brand">
|
||||||
<a
|
<a
|
||||||
v-tooltip="primaryFile.filename + ' (' + $formatBytes(primaryFile.size) + ')'"
|
v-tooltip="primaryFile.filename + ' (' + formatBytes(primaryFile.size) + ')'"
|
||||||
:href="primaryFile.url"
|
:href="primaryFile.url"
|
||||||
@click="emit('onDownload')"
|
@click="emit('onDownload')"
|
||||||
>
|
>
|
||||||
@@ -320,7 +320,7 @@
|
|||||||
<FileIcon aria-hidden="true" />
|
<FileIcon aria-hidden="true" />
|
||||||
<span class="filename">
|
<span class="filename">
|
||||||
<strong>{{ replaceFile.name }}</strong>
|
<strong>{{ replaceFile.name }}</strong>
|
||||||
<span class="file-size">({{ $formatBytes(replaceFile.size) }})</span>
|
<span class="file-size">({{ formatBytes(replaceFile.size) }})</span>
|
||||||
</span>
|
</span>
|
||||||
<FileInput
|
<FileInput
|
||||||
class="iconified-button raised-button"
|
class="iconified-button raised-button"
|
||||||
@@ -345,7 +345,7 @@
|
|||||||
<FileIcon aria-hidden="true" />
|
<FileIcon aria-hidden="true" />
|
||||||
<span class="filename">
|
<span class="filename">
|
||||||
<strong>{{ file.filename }}</strong>
|
<strong>{{ file.filename }}</strong>
|
||||||
<span class="file-size">({{ $formatBytes(file.size) }})</span>
|
<span class="file-size">({{ formatBytes(file.size) }})</span>
|
||||||
<span v-if="primaryFile.hashes.sha1 === file.hashes.sha1" class="file-type">
|
<span v-if="primaryFile.hashes.sha1 === file.hashes.sha1" class="file-type">
|
||||||
Primary
|
Primary
|
||||||
</span>
|
</span>
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
<FileIcon aria-hidden="true" />
|
<FileIcon aria-hidden="true" />
|
||||||
<span class="filename">
|
<span class="filename">
|
||||||
<strong>{{ file.name }}</strong>
|
<strong>{{ file.name }}</strong>
|
||||||
<span class="file-size">({{ $formatBytes(file.size) }})</span>
|
<span class="file-size">({{ formatBytes(file.size) }})</span>
|
||||||
</span>
|
</span>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-if="version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))"
|
v-if="version.loaders.some((x) => tags.loaderData.dataPackLoaders.includes(x))"
|
||||||
@@ -533,7 +533,7 @@
|
|||||||
)
|
)
|
||||||
.map((it) => it.name)
|
.map((it) => it.name)
|
||||||
"
|
"
|
||||||
:custom-label="(value) => $formatCategory(value)"
|
:custom-label="formatCategory"
|
||||||
:loading="tags.loaders.length === 0"
|
:loading="tags.loaders.length === 0"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
@@ -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,
|
||||||
@@ -649,6 +657,7 @@ import {
|
|||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { Multiselect } from "vue-multiselect";
|
import { Multiselect } from "vue-multiselect";
|
||||||
|
import { formatBytes, formatCategory } from "@modrinth/utils";
|
||||||
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
|
||||||
import { inferVersionInfo } from "~/helpers/infer.js";
|
import { inferVersionInfo } from "~/helpers/infer.js";
|
||||||
import { createDataPackVersion } from "~/helpers/package.js";
|
import { createDataPackVersion } from "~/helpers/package.js";
|
||||||
@@ -656,13 +665,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";
|
||||||
|
|
||||||
@@ -958,6 +963,8 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
formatBytes,
|
||||||
|
formatCategory,
|
||||||
async onImageUpload(file) {
|
async onImageUpload(file) {
|
||||||
const response = await useImageUpload(file, { context: "version" });
|
const response = await useImageUpload(file, { context: "version" });
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
v-if="notifTypes.length > 1"
|
v-if="notifTypes.length > 1"
|
||||||
v-model="selectedType"
|
v-model="selectedType"
|
||||||
:items="notifTypes"
|
:items="notifTypes"
|
||||||
:format-label="(x) => (x === 'all' ? 'All' : $formatProjectType(x).replace('_', ' ') + 's')"
|
:format-label="(x) => (x === 'all' ? 'All' : formatProjectType(x).replace('_', ' ') + 's')"
|
||||||
:capitalize="false"
|
:capitalize="false"
|
||||||
/>
|
/>
|
||||||
<p v-if="pending">Loading notifications...</p>
|
<p v-if="pending">Loading notifications...</p>
|
||||||
@@ -49,13 +49,16 @@
|
|||||||
/>
|
/>
|
||||||
</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 { formatProjectType } from "@modrinth/utils";
|
||||||
import {
|
import {
|
||||||
fetchExtraNotificationData,
|
fetchExtraNotificationData,
|
||||||
groupNotifications,
|
groupNotifications,
|
||||||
@@ -63,7 +66,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) {
|
||||||
|
|||||||
@@ -239,7 +239,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
: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
|
||||||
}`"
|
}`"
|
||||||
>
|
>
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
|
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
class="hover-link wrap-as-needed"
|
class="hover-link wrap-as-needed"
|
||||||
: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
|
||||||
}`"
|
}`"
|
||||||
>
|
>
|
||||||
@@ -275,22 +275,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{{ $formatProjectType($getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
{{ formatProjectType(getProjectTypeForUrl(project.project_type, project.loaders)) }}
|
||||||
</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,25 @@ 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 { formatProjectType } from "@modrinth/utils";
|
||||||
|
|
||||||
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";
|
import { getProjectTypeForUrl } from "~/helpers/projects.js";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
ButtonStyled,
|
||||||
|
ProjectStatusBadge,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -390,6 +397,8 @@ export default defineNuxtComponent({
|
|||||||
this.DELETE_PROJECT = 1 << 7;
|
this.DELETE_PROJECT = 1 << 7;
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getProjectTypeForUrl,
|
||||||
|
formatProjectType,
|
||||||
updateDescending() {
|
updateDescending() {
|
||||||
this.descending = !this.descending;
|
this.descending = !this.descending;
|
||||||
this.projects = this.updateSort(this.projects, this.sortBy, this.descending);
|
this.projects = this.updateSort(this.projects, this.sortBy, this.descending);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<template v-if="payout.method">
|
<template v-if="payout.method">
|
||||||
<span>⋅</span>
|
<span>⋅</span>
|
||||||
<span>{{ $formatWallet(payout.method) }} ({{ payout.method_address }})</span>
|
<span>{{ formatWallet(payout.method) }} ({{ payout.method_address }})</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { XIcon, PayPalIcon, UnknownIcon } from "@modrinth/assets";
|
import { XIcon, PayPalIcon, UnknownIcon } from "@modrinth/assets";
|
||||||
import { capitalizeString } from "@modrinth/utils";
|
import { capitalizeString, formatWallet } from "@modrinth/utils";
|
||||||
import { Badge, Breadcrumbs, DropdownSelect } from "@modrinth/ui";
|
import { Badge, Breadcrumbs, DropdownSelect } from "@modrinth/ui";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import TremendousIcon from "~/assets/images/external/tremendous.svg?component";
|
import TremendousIcon from "~/assets/images/external/tremendous.svg?component";
|
||||||
|
|||||||
@@ -139,8 +139,8 @@
|
|||||||
<template v-if="knownErrors.length === 0 && amount">
|
<template v-if="knownErrors.length === 0 && amount">
|
||||||
<Checkbox v-if="fees > 0" v-model="agreedFees" description="Consent to fee">
|
<Checkbox v-if="fees > 0" v-model="agreedFees" description="Consent to fee">
|
||||||
I acknowledge that an estimated
|
I acknowledge that an estimated
|
||||||
{{ $formatMoney(fees) }} will be deducted from the amount I receive to cover
|
{{ formatMoney(fees) }} will be deducted from the amount I receive to cover
|
||||||
{{ $formatWallet(selectedMethod.type) }} processing fees.
|
{{ formatWallet(selectedMethod.type) }} processing fees.
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox v-model="agreedTransfer" description="Confirm transfer">
|
<Checkbox v-model="agreedTransfer" description="Confirm transfer">
|
||||||
<template v-if="selectedMethod.type === 'tremendous'">
|
<template v-if="selectedMethod.type === 'tremendous'">
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
I confirm that I am initiating a transfer to the following
|
I confirm that I am initiating a transfer to the following
|
||||||
{{ $formatWallet(selectedMethod.type) }} account: {{ withdrawAccount }}
|
{{ formatWallet(selectedMethod.type) }} account: {{ withdrawAccount }}
|
||||||
</template>
|
</template>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox v-model="agreedTerms" class="rewards-checkbox">
|
<Checkbox v-model="agreedTerms" class="rewards-checkbox">
|
||||||
@@ -198,6 +198,7 @@ import {
|
|||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { Chips, Checkbox, Breadcrumbs } from "@modrinth/ui";
|
import { Chips, Checkbox, Breadcrumbs } from "@modrinth/ui";
|
||||||
import { all } from "iso-3166-1";
|
import { all } from "iso-3166-1";
|
||||||
|
import { formatMoney, formatWallet } from "@modrinth/utils";
|
||||||
import VenmoIcon from "~/assets/images/external/venmo.svg?component";
|
import VenmoIcon from "~/assets/images/external/venmo.svg?component";
|
||||||
|
|
||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
@@ -360,9 +361,7 @@ async function withdraw() {
|
|||||||
text:
|
text:
|
||||||
selectedMethod.value.type === "tremendous"
|
selectedMethod.value.type === "tremendous"
|
||||||
? "An email has been sent to your account with further instructions on how to redeem your payout!"
|
? "An email has been sent to your account with further instructions on how to redeem your payout!"
|
||||||
: `Payment has been sent to your ${data.$formatWallet(
|
: `Payment has been sent to your ${formatWallet(selectedMethod.value.type)} account!`,
|
||||||
selectedMethod.value.type,
|
|
||||||
)} account!`,
|
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { formatNumber } from "~/plugins/shorthands.js";
|
import { formatNumber } from "@modrinth/utils";
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: "Staff overview - Modrinth",
|
title: "Staff overview - Modrinth",
|
||||||
|
|||||||
Loaded 100 of 372 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user