Compare commits
125 Commits
v0.9.4
...
cal/dev-76
| 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 | ||
|
|
a3839461cf | ||
|
|
858c7e393f | ||
|
|
0278241006 | ||
|
|
3afb682fc6 | ||
|
|
06f1df1995 | ||
|
|
3489771d2e | ||
|
|
448ae5a2b7 | ||
|
|
72340790e5 | ||
|
|
c9423fe478 | ||
|
|
02a850ae63 | ||
|
|
ede6d0c3cc | ||
|
|
7685989a8c | ||
|
|
4e8ebb5e5c | ||
|
|
3f77ab19ed | ||
|
|
d3d0c8c523 | ||
|
|
4e093131f3 | ||
|
|
6ca8a4e5fd | ||
|
|
63b15ded60 | ||
|
|
85e65aeffe | ||
|
|
ad44398492 | ||
|
|
a4ba41bf15 | ||
|
|
4441be5380 | ||
|
|
c0accb42fa | ||
|
|
7223c2b197 | ||
|
|
7b535a1c2a | ||
|
|
0aa76567a6 | ||
|
|
6fa1369c49 | ||
|
|
b66d99c59c | ||
|
|
a9cfc37aac | ||
|
|
be37f077d3 | ||
|
|
f52d020a3c | ||
|
|
74cf3f076e | ||
|
|
84adf79564 | ||
|
|
dc0d923cee | ||
|
|
2ffd7476aa | ||
|
|
034fd06284 | ||
|
|
49aac6bdca | ||
|
|
4e4a7be7ef | ||
|
|
9c1bdf16e4 | ||
|
|
9e527ff141 | ||
|
|
c6022ad977 | ||
|
|
ea9a3539eb | ||
|
|
e4f0dddf82 | ||
|
|
be425cff6f | ||
|
|
e225bc9f66 | ||
|
|
f19643095e | ||
|
|
37cc81a36d | ||
|
|
863bf62f8d | ||
|
|
9a6390bb4d | ||
|
|
62de07e4e6 | ||
|
|
6e46317a37 | ||
|
|
b59f208e91 | ||
|
|
8fdc7403b1 | ||
|
|
895cd11e30 | ||
|
|
58ce3a4967 | ||
|
|
bdd4deb302 | ||
|
|
ec2a56cd73 | ||
|
|
10a5864a47 | ||
|
|
16766be82f | ||
|
|
1884410e0d | ||
|
|
6d57da2053 | ||
|
|
e4adbb9469 | ||
|
|
8ee621295c | ||
|
|
32920dd825 | ||
|
|
2d5d2d5df8 | ||
|
|
8dd32bbe98 | ||
|
|
f71830e0fa | ||
|
|
bd0d6a9ac0 | ||
|
|
dfef0df464 | ||
|
|
9821737431 | ||
|
|
f932ce7706 | ||
|
|
de3019e92b | ||
|
|
20b616a7c4 | ||
|
|
4180544e0a | ||
|
|
9168d349fc | ||
|
|
3dad6b317f | ||
|
|
4a2605bc1e | ||
|
|
41543e3af0 | ||
|
|
6003f1a10e | ||
|
|
3d9be0cc3f | ||
|
|
5e7444f115 | ||
|
|
20fcf70e90 | ||
|
|
0508f13cb6 | ||
|
|
2f68c62b3a | ||
|
|
ea64e08791 | ||
|
|
6f485d62ad |
@@ -1,6 +1,6 @@
|
||||
# Windows has stack overflows when calling from Tauri, so we increase compiler size
|
||||
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||
[target.'cfg(windows)']
|
||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
|
||||
25
.github/CODEOWNERS
vendored
Normal file
@@ -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
|
||||
2
.github/ISSUE_TEMPLATE/1-app-bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
||||
attributes:
|
||||
label: Please confirm the following.
|
||||
options:
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate problems
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate problems
|
||||
required: true
|
||||
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/2-web-bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
||||
attributes:
|
||||
label: Please confirm the following.
|
||||
options:
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate problems
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate problems
|
||||
required: true
|
||||
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/3-api-bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
||||
attributes:
|
||||
label: Please confirm the following.
|
||||
options:
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate problems
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate problems
|
||||
required: true
|
||||
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
||||
required: true
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/4-feature-request.yml
vendored
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
label: Please confirm the following.
|
||||
options:
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues) for duplicate feature requests
|
||||
- label: I checked the [existing issues](https://github.com/modrinth/code/issues?q=is%3Aissue) for duplicate feature requests
|
||||
required: true
|
||||
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
|
||||
required: true
|
||||
|
||||
BIN
.github/assets/api_cover.png
vendored
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 8.0 KiB |
BIN
.github/assets/app_cover.png
vendored
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 17 KiB |
BIN
.github/assets/monorepo_cover.png
vendored
|
Before Width: | Height: | Size: 417 KiB After Width: | Height: | Size: 262 KiB |
BIN
.github/assets/web_cover.png
vendored
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 24 KiB |
14
.github/workflows/theseus-release.yml
vendored
@@ -32,16 +32,16 @@ jobs:
|
||||
|
||||
- name: Rust setup (mac)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||
rustflags: ''
|
||||
target: x86_64-apple-darwin
|
||||
|
||||
- name: Rust setup
|
||||
if: "!startsWith(matrix.platform, 'macos')"
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
rustflags: ''
|
||||
|
||||
- name: Setup rust cache
|
||||
uses: actions/cache@v4
|
||||
@@ -72,10 +72,10 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-rust-target-
|
||||
|
||||
- name: Use Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
|
||||
98
.github/workflows/turbo-ci.yml
vendored
@@ -2,7 +2,7 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
merge_group:
|
||||
@@ -10,71 +10,69 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build, Test, and Lint
|
||||
name: Lint and Test
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
env:
|
||||
# Ensure pnpm output is colored in GitHub Actions logs
|
||||
FORCE_COLOR: 3
|
||||
# Make cargo nextest successfully ignore projects without tests
|
||||
NEXTEST_NO_TESTS: pass
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Cache turbo build setup
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo
|
||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-turbo-
|
||||
|
||||
- name: Install build dependencies
|
||||
- name: 🧰 Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Setup Node.JS environment
|
||||
- name: 🧰 Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🧰 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
- name: 🧰 Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
rustflags: ''
|
||||
components: clippy, rustfmt
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
- name: 🧰 Setup nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||
# back to a cached cargo install
|
||||
- name: 🧰 Setup cargo-sqlx
|
||||
uses: AlexTMjugador/cache-cargo-install-action@feat/features-support
|
||||
with:
|
||||
tool: sqlx-cli
|
||||
locked: false
|
||||
no-default-features: true
|
||||
features: rustls,postgres
|
||||
|
||||
- name: 💨 Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@v1.8
|
||||
|
||||
- name: 🧰 Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
- name: ⚙️ Start services
|
||||
run: docker compose up --wait
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
- name: ⚙️ Setup Labrinth environment and database
|
||||
working-directory: apps/labrinth
|
||||
run: |
|
||||
cp .env.local .env
|
||||
sqlx database setup
|
||||
|
||||
- name: Start docker compose
|
||||
run: docker compose up -d
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
||||
- name: 🔍 Lint and test
|
||||
run: pnpm run ci
|
||||
|
||||
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CommitMessageInspectionProfile">
|
||||
<profile version="1.0">
|
||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
|
||||
1
.vscode/settings.json
vendored
@@ -2,6 +2,7 @@
|
||||
"prettier.endOfLine": "lf",
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||
"editor.detectIndentation": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
|
||||
5150
Cargo.lock
generated
233
Cargo.toml
@@ -1,25 +1,226 @@
|
||||
[workspace]
|
||||
resolver = '2'
|
||||
resolver = "2"
|
||||
members = [
|
||||
'./packages/app-lib',
|
||||
'./apps/app-playground',
|
||||
'./apps/app',
|
||||
'./apps/labrinth',
|
||||
'./apps/daedalus_client',
|
||||
'./packages/daedalus',
|
||||
'./packages/ariadne',
|
||||
"apps/app",
|
||||
"apps/app-playground",
|
||||
"apps/daedalus_client",
|
||||
"apps/labrinth",
|
||||
"packages/app-lib",
|
||||
"packages/ariadne",
|
||||
"packages/daedalus",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
actix-cors = "0.7.1"
|
||||
actix-files = "0.6.6"
|
||||
actix-http = "3.11.0"
|
||||
actix-multipart = "0.7.2"
|
||||
actix-rt = "2.10.0"
|
||||
actix-web = "4.11.0"
|
||||
actix-web-prom = "0.10.0"
|
||||
actix-ws = "0.3.0"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
ariadne = { path = "packages/ariadne" }
|
||||
async_zip = "0.0.17"
|
||||
async-compression = { version = "0.4.24", default-features = false }
|
||||
async-recursion = "1.1.1"
|
||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||
"runtime-tokio-hyper-rustls",
|
||||
] }
|
||||
async-trait = "0.1.88"
|
||||
async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
||||
"futures-03-sink",
|
||||
] }
|
||||
async-walkdir = "2.1.0"
|
||||
base64 = "0.22.1"
|
||||
bitflags = "2.9.1"
|
||||
bytes = "1.10.1"
|
||||
censor = "0.3.0"
|
||||
chardetng = "0.1.17"
|
||||
chrono = "0.4.41"
|
||||
clap = "4.5.40"
|
||||
clickhouse = "0.13.3"
|
||||
color-thief = "0.2.2"
|
||||
console-subscriber = "0.4.1"
|
||||
daedalus = { path = "packages/daedalus" }
|
||||
dashmap = "6.1.0"
|
||||
deadpool-redis = "0.21.1"
|
||||
dirs = "6.0.0"
|
||||
discord-rich-presence = "0.2.5"
|
||||
dotenv-build = "0.1.1"
|
||||
dotenvy = "0.15.7"
|
||||
dunce = "1.0.5"
|
||||
either = "1.15.0"
|
||||
encoding_rs = "0.8.35"
|
||||
enumset = "1.1.6"
|
||||
flate2 = "1.1.2"
|
||||
fs4 = { version = "0.13.1", default-features = false }
|
||||
futures = { version = "0.3.31", default-features = false }
|
||||
futures-util = "0.3.31"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25.2"
|
||||
hmac = "0.12.1"
|
||||
hyper-tls = "0.6.0"
|
||||
hyper-util = "0.1.14"
|
||||
iana-time-zone = "0.1.63"
|
||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||
indexmap = "2.9.0"
|
||||
indicatif = "0.17.11"
|
||||
itertools = "0.14.0"
|
||||
jemalloc_pprof = "0.7.0"
|
||||
json-patch = { version = "4.0.0", default-features = false }
|
||||
lettre = { version = "0.11.17", default-features = false, features = [
|
||||
"builder",
|
||||
"hostname",
|
||||
"pool",
|
||||
"ring",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"smtp-transport",
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.28.0", default-features = false }
|
||||
murmur2 = "0.1.0"
|
||||
native-dialog = "0.9.0"
|
||||
notify = { version = "8.0.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
||||
p256 = "0.13.2"
|
||||
paste = "1.0.15"
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
quick-xml = "0.37.5"
|
||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12.19", default-features = false }
|
||||
rust_decimal = { version = "1.37.1", features = [
|
||||
"serde-with-float",
|
||||
"serde-with-str",
|
||||
] }
|
||||
rust_iso3166 = "0.1.14"
|
||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||
"fail-on-err",
|
||||
"tags",
|
||||
"tokio-rustls-tls",
|
||||
] }
|
||||
rusty-money = "0.4.1"
|
||||
sentry = { version = "0.38.1", default-features = false, features = [
|
||||
"backtrace",
|
||||
"contexts",
|
||||
"debug-images",
|
||||
"panic",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
] }
|
||||
sentry-actix = "0.38.1"
|
||||
serde = "1.0.219"
|
||||
serde_bytes = "0.11.17"
|
||||
serde_cbor = "0.11.2"
|
||||
serde_ini = "0.2.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_with = "3.12.0"
|
||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||
sha1 = "0.10.6"
|
||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||
sha2 = "0.10.9"
|
||||
spdx = "0.10.8"
|
||||
sqlx = { version = "0.8.6", default-features = false }
|
||||
sysinfo = { version = "0.35.2", default-features = false }
|
||||
tar = "0.4.44"
|
||||
tauri = "2.5.1"
|
||||
tauri-build = "2.2.0"
|
||||
tauri-plugin-deep-link = "2.3.0"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
tauri-plugin-opener = "2.2.7"
|
||||
tauri-plugin-os = "2.2.1"
|
||||
tauri-plugin-single-instance = "2.2.4"
|
||||
tauri-plugin-updater = { version = "2.7.1", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"zip",
|
||||
] }
|
||||
tauri-plugin-window-state = "2.2.2"
|
||||
tempfile = "3.20.0"
|
||||
theseus = { path = "packages/app-lib" }
|
||||
thiserror = "2.0.12"
|
||||
tikv-jemalloc-ctl = "0.6.0"
|
||||
tikv-jemallocator = "0.6.0"
|
||||
tokio = "1.45.1"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-util = "0.7.15"
|
||||
totp-rs = "5.7.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-actix-web = "0.7.18"
|
||||
tracing-error = "0.2.1"
|
||||
tracing-subscriber = "0.3.19"
|
||||
url = "2.5.4"
|
||||
urlencoding = "2.1.3"
|
||||
uuid = "1.17.0"
|
||||
validator = "0.20.0"
|
||||
webp = { version = "0.3.0", default-features = false }
|
||||
whoami = "1.6.0"
|
||||
winreg = "0.55.0"
|
||||
woothee = "0.13.0"
|
||||
yaserde = "0.12.0"
|
||||
zip = { version = "4.0.0", default-features = false, features = [
|
||||
"bzip2",
|
||||
"deflate",
|
||||
"deflate64",
|
||||
"zstd",
|
||||
] }
|
||||
zxcvbn = "3.1.0"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
bool_to_int_with_if = "warn"
|
||||
borrow_as_ptr = "warn"
|
||||
cfg_not_test = "warn"
|
||||
clear_with_drain = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
collection_is_never_read = "warn"
|
||||
dbg_macro = "warn"
|
||||
default_trait_access = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
filter_map_next = "warn"
|
||||
flat_map_option = "warn"
|
||||
format_push_string = "warn"
|
||||
get_unwrap = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
manual_assert = "warn"
|
||||
manual_instant_elapsed = "warn"
|
||||
manual_is_variant_and = "warn"
|
||||
manual_let_else = "warn"
|
||||
map_unwrap_or = "warn"
|
||||
match_bool = "warn"
|
||||
needless_collect = "warn"
|
||||
negative_feature_names = "warn"
|
||||
non_std_lazy_statics = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
read_zero_byte_vec = "warn"
|
||||
redundant_clone = "warn"
|
||||
redundant_feature_names = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
todo = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
|
||||
[workspace.lints.rust]
|
||||
# Turn warnings into errors by default
|
||||
warnings = "deny"
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "cafdaa9" }
|
||||
|
||||
# Optimize for speed and reduce size on release builds
|
||||
[profile.release]
|
||||
panic = "abort" # Strip expensive panic clean-up logic
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
lto = true # Enables link to optimizations
|
||||
opt-level = "s" # Optimize for binary size
|
||||
strip = true # Remove debug symbols
|
||||
opt-level = "s" # Optimize for binary size
|
||||
strip = true # Remove debug symbols
|
||||
lto = true # Enables link to optimizations
|
||||
panic = "abort" # Strip expensive panic clean-up logic
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
wry = { git = "https://github.com/modrinth/wry", rev = "51907c6" }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -9,20 +9,21 @@
|
||||
"tsc:check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"test": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@sentry/vue": "^8.27.0",
|
||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-os": "^2.2.0",
|
||||
"@tauri-apps/plugin-opener": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.3.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.0",
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"floating-vue": "^5.2.2",
|
||||
@@ -39,11 +40,12 @@
|
||||
"@eslint/compat": "^1.1.1",
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@nuxt/eslint-config": "^0.5.6",
|
||||
"@taijased/vue-render-tracker": "^1.0.7",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"eslint-plugin-turbo": "^2.1.1",
|
||||
"eslint-plugin-turbo": "^2.5.4",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.74.1",
|
||||
@@ -51,8 +53,7 @@
|
||||
"tsconfig": "workspace:*",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.6",
|
||||
"vue-tsc": "^2.1.6",
|
||||
"@taijased/vue-render-tracker": "^1.0.7"
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
"web-types": "../../web-types.json"
|
||||
|
||||
@@ -19,12 +19,19 @@ import {
|
||||
WorldIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
ButtonStyled,
|
||||
Notifications,
|
||||
OverflowMenu,
|
||||
useRelativeTime,
|
||||
} from '@modrinth/ui'
|
||||
import { useLoading, useTheming } from '@/store/state'
|
||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
@@ -62,6 +69,8 @@ import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const news = ref([])
|
||||
@@ -590,7 +599,7 @@ function handleAuxClick(e) {
|
||||
</h4>
|
||||
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
|
||||
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
|
||||
{{ dayjs(item.date).fromNow() }}
|
||||
{{ formatRelativeTime(dayjs(item.date).toISOString()) }}
|
||||
</p>
|
||||
</a>
|
||||
<hr
|
||||
|
||||
BIN
apps/app-frontend/src/assets/external/gdlauncher.png
vendored
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 937 KiB After Width: | Height: | Size: 270 KiB |
@@ -7,7 +7,7 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/regular.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -15,7 +15,7 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 400;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/italic.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -23,7 +23,7 @@
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -31,7 +31,7 @@
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
font-weight: 600;
|
||||
src: url('https://cdn.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||
src: url('https://cdn-raw.modrinth.com/fonts/minecraft/bold-italic.otf') format('opentype');
|
||||
}
|
||||
|
||||
.font-minecraft {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ref, computed } from 'vue'
|
||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { cancel_directory_change } from '@/helpers/settings.js'
|
||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||
import { install } from '@/helpers/profile.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
StopCircleIcon,
|
||||
TimerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { finish_install, kill, run } from '@/helpers/profile'
|
||||
import { get_by_profile_path } from '@/helpers/process'
|
||||
@@ -19,10 +19,9 @@ import { showProfileInFolder } from '@/helpers/utils.js'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { formatCategory } from '@modrinth/utils'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
@@ -173,7 +172,9 @@ onUnmounted(() => unlisten())
|
||||
</div>
|
||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||
<TimerIcon />
|
||||
<span class="text-sm"> Played {{ dayjs(instance.last_played).fromNow() }} </span>
|
||||
<span class="text-sm">
|
||||
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ModalWrapper ref="modal" header="Create instance">
|
||||
<ModalWrapper ref="modal" header="Creating an instance">
|
||||
<div class="modal-header">
|
||||
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
|
||||
</div>
|
||||
|
||||
@@ -124,8 +124,11 @@ import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { ref, computed } from 'vue'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
backgroundImage: {
|
||||
type: String,
|
||||
@@ -168,6 +171,9 @@ async function install() {
|
||||
installing.value = false
|
||||
emit('install', props.project.project_id ?? props.project.id)
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
||||
import {
|
||||
UserPlusIcon,
|
||||
MoreVerticalIcon,
|
||||
@@ -18,6 +18,8 @@ import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const props = defineProps<{
|
||||
credentials: unknown | null
|
||||
signIn: () => void
|
||||
@@ -205,7 +207,9 @@ onUnmounted(() => {
|
||||
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||
</template>
|
||||
</p>
|
||||
<p class="m-0 text-sm text-secondary">{{ friend.created.fromNow() }}</p>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
{{ formatRelativeTime(friend.created.toISOString()) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<template v-if="friend.id === userCredentials.user_id">
|
||||
|
||||
@@ -13,15 +13,17 @@ const confirmModal = ref(null)
|
||||
const installing = ref(false)
|
||||
|
||||
const onInstall = ref(() => {})
|
||||
const onCreateInstance = ref(() => {})
|
||||
|
||||
defineExpose({
|
||||
show: (projectVal, versionIdVal, callback) => {
|
||||
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
||||
project.value = projectVal
|
||||
versionId.value = versionIdVal
|
||||
installing.value = false
|
||||
confirmModal.value.show()
|
||||
|
||||
onInstall.value = callback
|
||||
onCreateInstance.value = createInstanceCallback
|
||||
|
||||
trackEvent('PackInstallStart')
|
||||
},
|
||||
@@ -36,6 +38,7 @@ async function install() {
|
||||
versionId.value,
|
||||
project.value.title,
|
||||
project.value.icon_url,
|
||||
onCreateInstance.value,
|
||||
).catch(handleError)
|
||||
trackEvent('PackInstall', {
|
||||
id: project.value.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Checkbox } from '@modrinth/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
||||
|
||||
@@ -25,9 +25,8 @@ const editProfileObject = computed(() => {
|
||||
hooks?: Hooks
|
||||
} = {}
|
||||
|
||||
if (overrideHooks.value) {
|
||||
editProfile.hooks = hooks.value
|
||||
}
|
||||
// When hooks are not overridden per-instance, we want to clear them
|
||||
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||
|
||||
return editProfile
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Checkbox, Toggle } from '@modrinth/ui'
|
||||
import { computed, ref, type Ref, watch } from 'vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { get } from '@/helpers/settings'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import { edit } from '@/helpers/profile'
|
||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/
|
||||
import { useTheming } from '@/store/state'
|
||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ConfirmModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { NewModal as Modal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { ShareModal } from '@modrinth/ui'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
import { useTheming } from '@/store/theme.js'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { getOS } from '@/helpers/utils'
|
||||
import type { ColorTheme } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -24,13 +25,13 @@ watch(
|
||||
|
||||
<ThemeSelector
|
||||
:update-color-theme="
|
||||
(theme) => {
|
||||
(theme: ColorTheme) => {
|
||||
themeStore.setThemeState(theme)
|
||||
settings.theme = theme
|
||||
}
|
||||
"
|
||||
:current-theme="settings.theme"
|
||||
:theme-options="themeStore.themeOptions"
|
||||
:theme-options="themeStore.getThemeOptions()"
|
||||
system-theme-color="system"
|
||||
/>
|
||||
|
||||
@@ -80,10 +81,28 @@ watch(
|
||||
id="opening-page"
|
||||
v-model="settings.default_page"
|
||||
name="Opening page dropdown"
|
||||
class="w-40"
|
||||
:options="['Home', 'Library']"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Jump back into worlds</h2>
|
||||
<p class="m-0 mt-1">Includes recent worlds in the "Jump back in" section on the Home page.</p>
|
||||
</div>
|
||||
<Toggle
|
||||
:model-value="themeStore.getFeatureFlag('worlds_in_home')"
|
||||
@update:model-value="
|
||||
() => {
|
||||
const newValue = !themeStore.getFeatureFlag('worlds_in_home')
|
||||
themeStore.featureFlags['worlds_in_home'] = newValue
|
||||
settings.feature_flags['worlds_in_home'] = newValue
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { handleError } from '@/store/notifications'
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { useTheming } from '@/store/state'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
||||
|
||||
const themeStore = useTheming()
|
||||
|
||||
const settings = ref(await get())
|
||||
const options = ref(['project_background', 'page_path', 'worlds_tab'])
|
||||
const settings = ref(await getSettings())
|
||||
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
||||
|
||||
function getStoreValue(key: string) {
|
||||
return themeStore.featureFlags[key] ?? false
|
||||
}
|
||||
|
||||
function setStoreValue(key: string, value: boolean) {
|
||||
function setFeatureFlag(key: string, value: boolean) {
|
||||
themeStore.featureFlags[key] = value
|
||||
settings.value.feature_flags[key] = value
|
||||
}
|
||||
@@ -21,7 +18,7 @@ function setStoreValue(key: string, value: boolean) {
|
||||
watch(
|
||||
settings,
|
||||
async () => {
|
||||
await set(settings.value)
|
||||
await setSettings(settings.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@@ -36,8 +33,8 @@ watch(
|
||||
|
||||
<Toggle
|
||||
id="advanced-rendering"
|
||||
:model-value="getStoreValue(option)"
|
||||
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
|
||||
:model-value="themeStore.getFeatureFlag(option)"
|
||||
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { Toggle } from '@modrinth/ui'
|
||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { Button, Slider } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get, set } from '@/helpers/settings.js'
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { purge_cache_types } from '@/helpers/cache.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||
|
||||
@@ -8,7 +8,14 @@ import {
|
||||
SpinnerIcon,
|
||||
StopCircleIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
useRelativeTime,
|
||||
} from '@modrinth/ui'
|
||||
import { useVIntl } from '@vintl/vintl'
|
||||
import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { showProfileInFolder } from '@/helpers/utils'
|
||||
@@ -25,6 +32,7 @@ import { handleError } from '@/store/notifications'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -144,7 +152,7 @@ onUnmounted(() => {
|
||||
<template v-if="instance.last_played">
|
||||
{{
|
||||
formatMessage(commonMessages.playedLabel, {
|
||||
time: dayjs(instance.last_played).fromNow(),
|
||||
time: formatRelativeTime(instance.last_played.toISOString()),
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
@@ -153,7 +161,7 @@ onUnmounted(() => {
|
||||
•
|
||||
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
||||
<router-link
|
||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary"
|
||||
class="inline-flex items-center gap-1 truncate hover:underline text-secondary smart-clickable:allow-pointer-events"
|
||||
:to="`/project/${modpack.id}`"
|
||||
>
|
||||
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
||||
|
||||
@@ -13,16 +13,17 @@ import {
|
||||
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||
import { watch, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch, onMounted, onUnmounted, ref, computed } from 'vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTheming } from '@/store/theme'
|
||||
import { kill } from '@/helpers/profile'
|
||||
import { useTheming } from '@/store/theme.ts'
|
||||
import { kill, run } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener, profile_listener } from '@/helpers/events'
|
||||
import { get_all } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
|
||||
const props = defineProps<{
|
||||
recentInstances: GameInstance[]
|
||||
@@ -54,7 +55,9 @@ type WorldJumpBackInItem = BaseJumpBackInItem & {
|
||||
|
||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
||||
|
||||
watch(props.recentInstances, async () => {
|
||||
const showWorlds = computed(() => theme.getFeatureFlag('worlds_in_home'))
|
||||
|
||||
watch([() => props.recentInstances, () => showWorlds.value], async () => {
|
||||
await populateJumpBackIn().catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
@@ -66,66 +69,71 @@ await populateJumpBackIn().catch(() => {
|
||||
|
||||
async function populateJumpBackIn() {
|
||||
console.info('Repopulating jump back in...')
|
||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN)
|
||||
|
||||
const worldItems: WorldJumpBackInItem[] = []
|
||||
worlds.forEach((world) => {
|
||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||
|
||||
if (!instance || !world.last_played) {
|
||||
return
|
||||
}
|
||||
if (showWorlds.value) {
|
||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
||||
|
||||
worldItems.push({
|
||||
type: 'world',
|
||||
last_played: dayjs(world.last_played),
|
||||
world: world,
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
worlds.forEach((world) => {
|
||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||
|
||||
const servers: {
|
||||
instancePath: string
|
||||
address: string
|
||||
}[] = worldItems
|
||||
.filter((item) => item.world.type === 'server' && item.instance)
|
||||
.map((item) => ({
|
||||
instancePath: item.instance.path,
|
||||
address: (item.world as ServerWorld).address,
|
||||
}))
|
||||
|
||||
// fetch protocol versions for all unique MC versions with server worlds
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||
await Promise.all(
|
||||
[...uniqueServerInstances].map((path) => {
|
||||
get_profile_protocol_version(path)
|
||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||
.catch(() => {
|
||||
console.error(`Failed to get profile protocol for: ${path} `)
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// initialize server data
|
||||
servers.forEach(({ address }) => {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
if (!instance || !world.last_played) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
await Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
)
|
||||
worldItems.push({
|
||||
type: 'world',
|
||||
last_played: dayjs(world.last_played),
|
||||
world: world,
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
|
||||
const servers: {
|
||||
instancePath: string
|
||||
address: string
|
||||
}[] = worldItems
|
||||
.filter((item) => item.world.type === 'server' && item.instance)
|
||||
.map((item) => ({
|
||||
instancePath: item.instance.path,
|
||||
address: (item.world as ServerWorld).address,
|
||||
}))
|
||||
|
||||
// fetch protocol versions for all unique MC versions with server worlds
|
||||
const uniqueServerInstances = new Set<string>(servers.map((x) => x.instancePath))
|
||||
await Promise.all(
|
||||
[...uniqueServerInstances].map((path) =>
|
||||
get_profile_protocol_version(path)
|
||||
.then((protoVer) => (protocolVersions.value[path] = protoVer))
|
||||
.catch(() => {
|
||||
console.error(`Failed to get profile protocol for: ${path} `)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// initialize server data
|
||||
servers.forEach(({ address }) => {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// fetch each server's data
|
||||
Promise.all(
|
||||
servers.map(({ instancePath, address }) =>
|
||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const instanceItems: InstanceJumpBackInItem[] = []
|
||||
props.recentInstances.forEach((instance) => {
|
||||
if (worldItems.some((item) => item.instance.path === instance.path) || !instance.last_played) {
|
||||
return
|
||||
for (const instance of props.recentInstances) {
|
||||
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
||||
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
||||
continue
|
||||
}
|
||||
|
||||
instanceItems.push({
|
||||
@@ -133,13 +141,13 @@ async function populateJumpBackIn() {
|
||||
last_played: dayjs(instance.last_played),
|
||||
instance: instance,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
||||
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
|
||||
jumpBackInItems.value = items.filter(
|
||||
(item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO),
|
||||
)
|
||||
jumpBackInItems.value = items
|
||||
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
|
||||
.slice(0, MAX_JUMP_BACK_IN)
|
||||
}
|
||||
|
||||
async function refreshServer(address: string, instancePath: string) {
|
||||
@@ -155,6 +163,18 @@ async function joinWorld(world: WorldWithProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
async function playInstance(instance: GameInstance) {
|
||||
await run(instance.path)
|
||||
.catch((err) => handleSevereError(err, { profilePath: instance.path }))
|
||||
.finally(() => {
|
||||
trackEvent('InstancePlay', {
|
||||
loader: instance.loader,
|
||||
game_version: instance.game_version,
|
||||
source: 'WorldItem',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function stopInstance(path: string) {
|
||||
await kill(path).catch(handleError)
|
||||
trackEvent('InstanceStop', {
|
||||
@@ -209,11 +229,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||
<HeadingLink
|
||||
v-if="(theme.featureFlags as Record<string, boolean>)['worlds_tab']"
|
||||
to="/worlds"
|
||||
class="mt-1"
|
||||
>
|
||||
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||
Jump back in
|
||||
</HeadingLink>
|
||||
<span
|
||||
@@ -222,7 +238,7 @@ onUnmounted(() => {
|
||||
>
|
||||
Jump back in
|
||||
</span>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||
<template
|
||||
v-for="item in jumpBackInItems"
|
||||
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||
@@ -246,7 +262,7 @@ onUnmounted(() => {
|
||||
:rendered-motd="
|
||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
||||
"
|
||||
:current-protocol="protocolVersions[item.instance.game_version]"
|
||||
:current-protocol="protocolVersions[item.instance.path]"
|
||||
:game-mode="
|
||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||
"
|
||||
@@ -259,6 +275,7 @@ onUnmounted(() => {
|
||||
? refreshServer(item.world.address, item.instance.path)
|
||||
: {}
|
||||
"
|
||||
@update="() => populateJumpBackIn()"
|
||||
@play="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
@@ -266,6 +283,12 @@ onUnmounted(() => {
|
||||
joinWorld(item.world)
|
||||
}
|
||||
"
|
||||
@play-instance="
|
||||
() => {
|
||||
currentProfile = item.instance.path
|
||||
playInstance(item.instance)
|
||||
}
|
||||
"
|
||||
@stop="() => stopInstance(item.instance.path)"
|
||||
/>
|
||||
<InstanceItem v-else :instance="item.instance" />
|
||||
@@ -273,3 +296,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.grid-when-huge {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(670px, 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
||||
import { getWorldIdentifier, showWorldInFolder } from '@/helpers/worlds.ts'
|
||||
import { formatNumber } from '@modrinth/utils'
|
||||
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||
import {
|
||||
useRelativeTime,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
OverflowMenu,
|
||||
SmartClickable,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
IssuesIcon,
|
||||
EyeIcon,
|
||||
@@ -19,8 +27,8 @@ import {
|
||||
TrashIcon,
|
||||
UpdatedIcon,
|
||||
UserIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
|
||||
import type { MessageDescriptor } from '@vintl/vintl'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import type { Component } from 'vue'
|
||||
@@ -31,11 +39,13 @@ import { useRouter } from 'vue-router'
|
||||
import { Tooltip } from 'floating-vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'play' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
(e: 'play' | 'play-instance' | 'update' | 'stop' | 'refresh' | 'edit' | 'delete'): void
|
||||
(e: 'open-folder', world: SingleplayerWorld): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -69,6 +79,7 @@ const props = withDefaults(
|
||||
playingWorld: false,
|
||||
startingInstance: false,
|
||||
supportsQuickPlay: false,
|
||||
currentProtocol: null,
|
||||
|
||||
refreshing: false,
|
||||
serverStatus: undefined,
|
||||
@@ -94,20 +105,6 @@ const serverIncompatible = computed(
|
||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||
)
|
||||
|
||||
function getPingLevel(ping: number) {
|
||||
if (ping < 150) {
|
||||
return 5
|
||||
} else if (ping < 300) {
|
||||
return 4
|
||||
} else if (ping < 600) {
|
||||
return 3
|
||||
} else if (ping < 1000) {
|
||||
return 2
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -143,10 +140,18 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.play_anyway',
|
||||
defaultMessage: 'Play anyway',
|
||||
},
|
||||
playInstance: {
|
||||
id: 'instance.worlds.play_instance',
|
||||
defaultMessage: 'Play instance',
|
||||
},
|
||||
worldInUse: {
|
||||
id: 'instance.worlds.world_in_use',
|
||||
defaultMessage: 'World is in use',
|
||||
},
|
||||
dontShowOnHome: {
|
||||
id: 'instance.worlds.dont_show_on_home',
|
||||
defaultMessage: `Don't show on Home`,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
@@ -241,7 +246,7 @@ const messages = defineMessages({
|
||||
<template v-if="world.last_played">
|
||||
{{
|
||||
formatMessage(commonMessages.playedLabel, {
|
||||
time: dayjs(world.last_played).fromNow(),
|
||||
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
@@ -336,6 +341,12 @@ const messages = defineMessages({
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
:options="[
|
||||
{
|
||||
id: 'play-instance',
|
||||
shown: !!instancePath,
|
||||
disabled: playingInstance,
|
||||
action: () => emit('play-instance'),
|
||||
},
|
||||
{
|
||||
id: 'play-anyway',
|
||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
||||
@@ -344,7 +355,7 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'open-instance',
|
||||
shown: !!instancePath,
|
||||
action: () => router.push(encodeURI(`/instance/${instancePath}/worlds`)),
|
||||
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
||||
},
|
||||
{
|
||||
id: 'refresh',
|
||||
@@ -366,8 +377,25 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'open-folder',
|
||||
shown: world.type === 'singleplayer',
|
||||
action: () =>
|
||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
||||
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: !!instancePath,
|
||||
},
|
||||
{
|
||||
id: 'dont-show-on-home',
|
||||
shown: !!instancePath,
|
||||
action: () => {
|
||||
set_world_display_status(
|
||||
instancePath,
|
||||
world.type,
|
||||
getWorldIdentifier(world),
|
||||
'hidden',
|
||||
).then(() => {
|
||||
emit('update')
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
@@ -385,6 +413,10 @@ const messages = defineMessages({
|
||||
]"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #play-instance>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playInstance) }}
|
||||
</template>
|
||||
<template #play-anyway>
|
||||
<PlayIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.playAnyway) }}
|
||||
@@ -406,6 +438,10 @@ const messages = defineMessages({
|
||||
<template #refresh>
|
||||
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
||||
</template>
|
||||
<template #dont-show-on-home>
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.dontShowOnHome) }}
|
||||
</template>
|
||||
<template #delete>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
{{
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { edit_server_in_profile, type ServerWorld } from '@/helpers/worlds.ts'
|
||||
import {
|
||||
type ServerPackStatus,
|
||||
edit_server_in_profile,
|
||||
type ServerWorld,
|
||||
set_world_display_status,
|
||||
type DisplayStatus,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -21,10 +28,14 @@ const props = defineProps<{
|
||||
|
||||
const modal = ref()
|
||||
|
||||
const name = ref()
|
||||
const address = ref()
|
||||
const resourcePack = ref('enabled')
|
||||
const index = ref()
|
||||
const name = ref<string>('')
|
||||
const address = ref<string>('')
|
||||
const resourcePack = ref<ServerPackStatus>('enabled')
|
||||
const index = ref<number>(0)
|
||||
const displayStatus = ref<DisplayStatus>('normal')
|
||||
const hideFromHome = ref(false)
|
||||
|
||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||
|
||||
async function saveServer() {
|
||||
const serverName = name.value ? name.value : address.value
|
||||
@@ -36,12 +47,23 @@ async function saveServer() {
|
||||
address.value,
|
||||
resourcePackStatus,
|
||||
).catch(handleError)
|
||||
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
'server',
|
||||
address.value,
|
||||
newDisplayStatus.value,
|
||||
).catch(handleError)
|
||||
}
|
||||
|
||||
emit('submit', {
|
||||
name: serverName,
|
||||
type: 'server',
|
||||
index: index.value,
|
||||
address: address.value,
|
||||
pack_status: resourcePackStatus,
|
||||
display_status: newDisplayStatus.value,
|
||||
})
|
||||
hide()
|
||||
}
|
||||
@@ -51,6 +73,8 @@ function show(server: ServerWorld) {
|
||||
address.value = server.address
|
||||
resourcePack.value = server.pack_status
|
||||
index.value = server.index
|
||||
displayStatus.value = server.display_status
|
||||
hideFromHome.value = server.display_status === 'hidden'
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
@@ -75,6 +99,7 @@ const titleMessage = defineMessage({
|
||||
v-model:address="address"
|
||||
v-model:resource-pack="resourcePack"
|
||||
/>
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
<button :disabled="!address" @click="saveServer">
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
||||
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import type { SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||
import { rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
||||
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||
import { set_world_display_status, rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [path: string, name: string, removeIcon: boolean]
|
||||
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -25,6 +26,10 @@ const icon = ref()
|
||||
const name = ref()
|
||||
const path = ref()
|
||||
const removeIcon = ref(false)
|
||||
const displayStatus = ref<DisplayStatus>('normal')
|
||||
const hideFromHome = ref(false)
|
||||
|
||||
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||
|
||||
async function saveWorld() {
|
||||
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
||||
@@ -32,8 +37,16 @@ async function saveWorld() {
|
||||
if (removeIcon.value) {
|
||||
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
||||
}
|
||||
if (newDisplayStatus.value !== displayStatus.value) {
|
||||
await set_world_display_status(
|
||||
props.instance.path,
|
||||
'singleplayer',
|
||||
path.value,
|
||||
newDisplayStatus.value,
|
||||
)
|
||||
}
|
||||
|
||||
emit('submit', path.value, name.value, removeIcon.value)
|
||||
emit('submit', path.value, name.value, removeIcon.value, newDisplayStatus.value)
|
||||
hide()
|
||||
}
|
||||
|
||||
@@ -41,6 +54,9 @@ function show(world: SingleplayerWorld) {
|
||||
name.value = world.name
|
||||
path.value = world.path
|
||||
icon.value = world.icon
|
||||
displayStatus.value = world.display_status
|
||||
hideFromHome.value = world.display_status === 'hidden'
|
||||
removeIcon.value = false
|
||||
modal.value.show()
|
||||
}
|
||||
|
||||
@@ -87,6 +103,7 @@ const messages = defineMessages({
|
||||
class="w-full"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||
</div>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<ButtonStyled color="brand">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { computed } from 'vue'
|
||||
import { Checkbox } from '@modrinth/ui'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const value = defineModel<boolean>({ required: true })
|
||||
|
||||
const labelMessage = defineMessage({
|
||||
id: 'instance.edit-world.hide-from-home',
|
||||
defaultMessage: `Hide from the Home page`,
|
||||
})
|
||||
|
||||
const label = computed(() => formatMessage(labelMessage))
|
||||
</script>
|
||||
<template>
|
||||
<Checkbox v-model="value" :label="label" />
|
||||
</template>
|
||||
@@ -7,7 +7,13 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import { create } from './profile'
|
||||
|
||||
// Installs pack from a version ID
|
||||
export async function create_profile_and_install(projectId, versionId, packTitle, iconUrl) {
|
||||
export async function create_profile_and_install(
|
||||
projectId,
|
||||
versionId,
|
||||
packTitle,
|
||||
iconUrl,
|
||||
createInstanceCallback = () => {},
|
||||
) {
|
||||
const location = {
|
||||
type: 'fromVersionId',
|
||||
project_id: projectId,
|
||||
@@ -24,6 +30,7 @@ export async function create_profile_and_install(projectId, versionId, packTitle
|
||||
null,
|
||||
true,
|
||||
)
|
||||
createInstanceCallback(profile)
|
||||
|
||||
return await invoke('plugin:pack|pack_install', { location, profile })
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
// Settings object
|
||||
/*
|
||||
|
||||
Settings {
|
||||
"memory": MemorySettings,
|
||||
"game_resolution": [int int],
|
||||
"custom_java_args": [String ...],
|
||||
"custom_env_args" : [(string, string) ... ]>,
|
||||
"java_globals": Hash of (string, Path),
|
||||
"default_user": Uuid string (can be null),
|
||||
"hooks": Hooks,
|
||||
"max_concurrent_downloads": uint,
|
||||
"version": u32,
|
||||
"collapsed_navigation": bool,
|
||||
}
|
||||
|
||||
Memorysettings {
|
||||
"min": u32, can be null,
|
||||
"max": u32,
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// Get full settings object
|
||||
export async function get() {
|
||||
return await invoke('plugin:settings|settings_get')
|
||||
}
|
||||
|
||||
// Set full settings object
|
||||
export async function set(settings) {
|
||||
return await invoke('plugin:settings|settings_set', { settings })
|
||||
}
|
||||
|
||||
export async function cancel_directory_change() {
|
||||
return await invoke('plugin:settings|cancel_directory_change')
|
||||
}
|
||||
78
apps/app-frontend/src/helpers/settings.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* All theseus API calls return serialized values (both return values and errors);
|
||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
||||
|
||||
// Settings object
|
||||
/*
|
||||
|
||||
Settings {
|
||||
"memory": MemorySettings,
|
||||
"game_resolution": [int int],
|
||||
"custom_java_args": [String ...],
|
||||
"custom_env_args" : [(string, string) ... ]>,
|
||||
"java_globals": Hash of (string, Path),
|
||||
"default_user": Uuid string (can be null),
|
||||
"hooks": Hooks,
|
||||
"max_concurrent_downloads": uint,
|
||||
"version": u32,
|
||||
"collapsed_navigation": bool,
|
||||
}
|
||||
|
||||
Memorysettings {
|
||||
"min": u32, can be null,
|
||||
"max": u32,
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
export type AppSettings = {
|
||||
max_concurrent_downloads: number
|
||||
max_concurrent_writes: number
|
||||
|
||||
theme: ColorTheme
|
||||
default_page: 'home' | 'library'
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
toggle_sidebar: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
personalized_ads: boolean
|
||||
|
||||
onboarded: boolean
|
||||
|
||||
extra_launch_args: string[]
|
||||
custom_env_vars: [string, string][]
|
||||
memory: MemorySettings
|
||||
force_fullscreen: boolean
|
||||
game_resolution: WindowSize
|
||||
hide_on_process_start: boolean
|
||||
hooks: Hooks
|
||||
|
||||
custom_dir?: string | null
|
||||
prev_custom_dir?: string | null
|
||||
migrated: boolean
|
||||
|
||||
developer_mode: boolean
|
||||
feature_flags: Record<FeatureFlag, boolean>
|
||||
}
|
||||
|
||||
// Get full settings object
|
||||
export async function get() {
|
||||
return (await invoke('plugin:settings|settings_get')) as AppSettings
|
||||
}
|
||||
|
||||
// Set full settings object
|
||||
export async function set(settings: AppSettings) {
|
||||
return await invoke('plugin:settings|settings_set', { settings })
|
||||
}
|
||||
|
||||
export async function cancel_directory_change(): Promise<void> {
|
||||
return await invoke('plugin:settings|cancel_directory_change')
|
||||
}
|
||||
27
apps/app-frontend/src/helpers/types.d.ts
vendored
@@ -48,6 +48,32 @@ type LinkedData = {
|
||||
|
||||
type InstanceLoader = 'vanilla' | 'forge' | 'fabric' | 'quilt' | 'neoforge'
|
||||
|
||||
type ContentFile = {
|
||||
hash: string
|
||||
file_name: string
|
||||
size: number
|
||||
metadata?: FileMetadata
|
||||
update_version_id?: string
|
||||
project_type: ContentFileProjectType
|
||||
}
|
||||
|
||||
type FileMetadata = {
|
||||
project_id: string
|
||||
version_id: string
|
||||
}
|
||||
|
||||
type ContentFileProjectType = 'mod' | 'datapack' | 'resourcepack' | 'shaderpack'
|
||||
|
||||
type CacheBehaviour =
|
||||
// Serve expired data. If fetch fails / launcher is offline, errors are ignored
|
||||
| 'stale_while_revalidate_skip_offline'
|
||||
// Serve expired data, revalidate in background
|
||||
| 'stale_while_revalidate'
|
||||
// Must revalidate if data is expired
|
||||
| 'must_revalidate'
|
||||
// Ignore cache- always fetch updated data from origin
|
||||
| 'bypass'
|
||||
|
||||
type MemorySettings = {
|
||||
maximum: number
|
||||
}
|
||||
@@ -88,6 +114,7 @@ type AppSettings = {
|
||||
collapsed_navigation: boolean
|
||||
advanced_rendering: boolean
|
||||
native_decorations: boolean
|
||||
worlds_in_home: boolean
|
||||
|
||||
telemetry: boolean
|
||||
discord_rpc: boolean
|
||||
|
||||
@@ -9,8 +9,13 @@ type BaseWorld = {
|
||||
name: string
|
||||
last_played?: string
|
||||
icon?: string
|
||||
display_status: DisplayStatus
|
||||
type: WorldType
|
||||
}
|
||||
|
||||
export type WorldType = 'singleplayer' | 'server'
|
||||
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
|
||||
|
||||
export type SingleplayerWorld = BaseWorld & {
|
||||
type: 'singleplayer'
|
||||
path: string
|
||||
@@ -70,8 +75,11 @@ export type ServerData = {
|
||||
renderedMotd?: string
|
||||
}
|
||||
|
||||
export async function get_recent_worlds(limit: number): Promise<WorldWithProfile[]> {
|
||||
return await invoke('plugin:worlds|get_recent_worlds', { limit })
|
||||
export async function get_recent_worlds(
|
||||
limit: number,
|
||||
displayStatuses?: DisplayStatus[],
|
||||
): Promise<WorldWithProfile[]> {
|
||||
return await invoke('plugin:worlds|get_recent_worlds', { limit, displayStatuses })
|
||||
}
|
||||
|
||||
export async function get_profile_worlds(path: string): Promise<World[]> {
|
||||
@@ -85,6 +93,20 @@ export async function get_singleplayer_world(
|
||||
return await invoke('plugin:worlds|get_singleplayer_world', { instance, world })
|
||||
}
|
||||
|
||||
export async function set_world_display_status(
|
||||
instance: string,
|
||||
worldType: WorldType,
|
||||
worldId: string,
|
||||
displayStatus: DisplayStatus,
|
||||
): Promise<void> {
|
||||
return await invoke('plugin:worlds|set_world_display_status', {
|
||||
instance,
|
||||
worldType,
|
||||
worldId,
|
||||
displayStatus,
|
||||
})
|
||||
}
|
||||
|
||||
export async function rename_world(
|
||||
instance: string,
|
||||
world: string,
|
||||
@@ -230,12 +252,14 @@ export async function refreshServers(
|
||||
|
||||
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
||||
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
||||
const newWorld = await get_singleplayer_world(instancePath, worldPath)
|
||||
if (index !== -1) {
|
||||
worlds[index] = await get_singleplayer_world(instancePath, worldPath)
|
||||
sortWorlds(worlds)
|
||||
worlds[index] = newWorld
|
||||
} else {
|
||||
console.error(`Error refreshing world, could not find world at path ${worldPath}.`)
|
||||
console.info(`Adding new world at path: ${worldPath}.`)
|
||||
worlds.push(newWorld)
|
||||
}
|
||||
sortWorlds(worlds)
|
||||
}
|
||||
|
||||
export async function handleDefaultProfileUpdateEvent(
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
"instance.edit-server.title": {
|
||||
"message": "Edit server"
|
||||
},
|
||||
"instance.edit-world.hide-from-home": {
|
||||
"message": "Hide from the Home page"
|
||||
},
|
||||
"instance.edit-world.name": {
|
||||
"message": "Name"
|
||||
},
|
||||
@@ -362,6 +365,9 @@
|
||||
"instance.worlds.copy_address": {
|
||||
"message": "Copy address"
|
||||
},
|
||||
"instance.worlds.dont_show_on_home": {
|
||||
"message": "Don't show on Home"
|
||||
},
|
||||
"instance.worlds.filter.available": {
|
||||
"message": "Available"
|
||||
},
|
||||
@@ -377,6 +383,9 @@
|
||||
"instance.worlds.play_anyway": {
|
||||
"message": "Play anyway"
|
||||
},
|
||||
"instance.worlds.play_instance": {
|
||||
"message": "Play instance"
|
||||
},
|
||||
"instance.worlds.type.server": {
|
||||
"message": "Server"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,11 @@
|
||||
<template #actions>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled
|
||||
v-if="instance.install_stage.includes('installing')"
|
||||
v-if="
|
||||
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
||||
instance.install_stage,
|
||||
)
|
||||
"
|
||||
color="brand"
|
||||
size="large"
|
||||
>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||
<button
|
||||
v-for="filter in filterOptions"
|
||||
:key="filter"
|
||||
:key="`content-filter-${filter.id}`"
|
||||
:class="`px-2 py-1 rounded-full font-semibold leading-none border-none cursor-pointer active:scale-[0.97] duration-100 transition-all ${selectedFilters.includes(filter.id) ? 'bg-brand-highlight text-brand' : 'bg-bg-raised text-secondary'}`"
|
||||
@click="toggleArray(selectedFilters, filter.id)"
|
||||
>
|
||||
@@ -47,7 +47,7 @@
|
||||
path: x.path,
|
||||
disabled: x.disabled,
|
||||
filename: x.file_name,
|
||||
icon: x.icon,
|
||||
icon: x.icon ?? undefined,
|
||||
title: x.name,
|
||||
data: x,
|
||||
}
|
||||
@@ -156,7 +156,7 @@
|
||||
color-fill="text"
|
||||
hover-color-fill="text"
|
||||
>
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal.show()">
|
||||
<button class="w-max" :disabled="installing" @click="modpackVersionModal?.show()">
|
||||
<DownloadIcon /> Update pack
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -170,7 +170,7 @@
|
||||
>
|
||||
<button
|
||||
v-tooltip="`Update`"
|
||||
:disabled="(item.data as any).updating"
|
||||
:disabled="(item.data as ProjectListEntry).updating"
|
||||
@click="updateProject(item.data)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
@@ -276,6 +276,7 @@ import {
|
||||
RadialHeader,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
@@ -305,43 +306,18 @@ import { profile_listener } from '@/helpers/events.js'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||
import dayjs from 'dayjs'
|
||||
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
|
||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
offline: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
playing: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
const props = defineProps<{
|
||||
instance: GameInstance
|
||||
options: InstanceType<typeof ContextMenu>
|
||||
offline: boolean
|
||||
playing: boolean
|
||||
versions: Version[]
|
||||
installed: boolean
|
||||
}>()
|
||||
|
||||
type ProjectListEntryAuthor = {
|
||||
name: string
|
||||
@@ -356,13 +332,15 @@ type ProjectListEntry = {
|
||||
author: ProjectListEntryAuthor | null
|
||||
version: string | null
|
||||
file_name: string
|
||||
icon: string | null
|
||||
icon: string | undefined
|
||||
disabled: boolean
|
||||
updateVersion?: string
|
||||
outdated: boolean
|
||||
updated: dayjs.Dayjs
|
||||
project_type: string
|
||||
id?: string
|
||||
updating?: boolean
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const isPackLocked = computed(() => {
|
||||
@@ -375,17 +353,20 @@ const canUpdatePack = computed(() => {
|
||||
const exportModal = ref(null)
|
||||
|
||||
const projects = ref<ProjectListEntry[]>([])
|
||||
const selectedFiles = ref([])
|
||||
const selectedFiles = ref<string[]>([])
|
||||
const selectedProjects = computed(() =>
|
||||
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
|
||||
)
|
||||
|
||||
const selectionMap = ref(new Map())
|
||||
|
||||
const initProjects = async (cacheBehaviour?) => {
|
||||
const initProjects = async (cacheBehaviour?: CacheBehaviour) => {
|
||||
const newProjects: ProjectListEntry[] = []
|
||||
|
||||
const profileProjects = await get_projects(props.instance.path, cacheBehaviour)
|
||||
const profileProjects = (await get_projects(props.instance.path, cacheBehaviour)) as Record<
|
||||
string,
|
||||
ContentFile
|
||||
>
|
||||
const fetchProjects = []
|
||||
const fetchVersions = []
|
||||
|
||||
@@ -397,21 +378,21 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
|
||||
const [modrinthProjects, modrinthVersions] = await Promise.all([
|
||||
await get_project_many(fetchProjects).catch(handleError),
|
||||
await get_version_many(fetchVersions).catch(handleError),
|
||||
(await get_project_many(fetchProjects).catch(handleError)) as Project[],
|
||||
(await get_version_many(fetchVersions).catch(handleError)) as Version[],
|
||||
])
|
||||
|
||||
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
|
||||
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError),
|
||||
await get_organization_many(
|
||||
(await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError)) as TeamMember[][],
|
||||
(await get_organization_many(
|
||||
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
|
||||
).catch(handleError),
|
||||
).catch(handleError)) as Organization[],
|
||||
])
|
||||
|
||||
for (const [path, file] of Object.entries(profileProjects)) {
|
||||
if (file.metadata) {
|
||||
const project = modrinthProjects.find((x) => file.metadata.project_id === x.id)
|
||||
const version = modrinthVersions.find((x) => file.metadata.version_id === x.id)
|
||||
const project = modrinthProjects.find((x) => file.metadata?.project_id === x.id)
|
||||
const version = modrinthVersions.find((x) => file.metadata?.version_id === x.id)
|
||||
|
||||
if (project && version) {
|
||||
const org = project.organization
|
||||
@@ -420,7 +401,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
|
||||
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
|
||||
|
||||
let author: ProjectListEntryAuthor | null
|
||||
let author: ProjectListEntryAuthor | null = null
|
||||
if (org) {
|
||||
author = {
|
||||
name: org.name,
|
||||
@@ -429,13 +410,13 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
} else if (team) {
|
||||
const teamMember = team.find((x) => x.is_owner)
|
||||
author = {
|
||||
name: teamMember.user.username,
|
||||
slug: teamMember.user.username,
|
||||
type: 'user',
|
||||
if (teamMember) {
|
||||
author = {
|
||||
name: teamMember.user.username,
|
||||
slug: teamMember.user.username,
|
||||
type: 'user',
|
||||
}
|
||||
}
|
||||
} else {
|
||||
author = null
|
||||
}
|
||||
|
||||
newProjects.push({
|
||||
@@ -464,7 +445,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
author: null,
|
||||
version: null,
|
||||
file_name: file.file_name,
|
||||
icon: null,
|
||||
icon: undefined,
|
||||
disabled: file.file_name.endsWith('.disabled'),
|
||||
outdated: false,
|
||||
updated: dayjs(0),
|
||||
@@ -488,7 +469,7 @@ const initProjects = async (cacheBehaviour?) => {
|
||||
}
|
||||
await initProjects()
|
||||
|
||||
const modpackVersionModal = ref(null)
|
||||
const modpackVersionModal = ref<InstanceType<typeof ModpackVersionModal> | null>()
|
||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||
|
||||
const vintl = useVIntl()
|
||||
@@ -513,7 +494,7 @@ const messages = defineMessages({
|
||||
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
const options: FilterOption[] = []
|
||||
|
||||
const frequency = projects.value.reduce((map, item) => {
|
||||
const frequency = projects.value.reduce((map: Record<string, number>, item) => {
|
||||
map[item.project_type] = (map[item.project_type] || 0) + 1
|
||||
return map
|
||||
}, {})
|
||||
@@ -544,7 +525,7 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||
return options
|
||||
})
|
||||
|
||||
const selectedFilters = ref([])
|
||||
const selectedFilters = ref<string[]>([])
|
||||
const filteredProjects = computed(() => {
|
||||
const updatesFilter = selectedFilters.value.includes('updates')
|
||||
const disabledFilter = selectedFilters.value.includes('disabled')
|
||||
@@ -571,7 +552,7 @@ watch(filterOptions, () => {
|
||||
}
|
||||
})
|
||||
|
||||
function toggleArray(array, value) {
|
||||
function toggleArray<T>(array: T[], value: T) {
|
||||
if (array.includes(value)) {
|
||||
array.splice(array.indexOf(value), 1)
|
||||
} else {
|
||||
@@ -581,7 +562,7 @@ function toggleArray(array, value) {
|
||||
|
||||
const searchFilter = ref('')
|
||||
const selectAll = ref(false)
|
||||
const shareModal = ref(null)
|
||||
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
|
||||
const ascending = ref(true)
|
||||
const sortColumn = ref('Name')
|
||||
const currentPage = ref(1)
|
||||
@@ -622,7 +603,7 @@ const search = computed(() => {
|
||||
|
||||
watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1))
|
||||
|
||||
const sortProjects = (filter) => {
|
||||
const sortProjects = (filter: string) => {
|
||||
if (sortColumn.value === filter) {
|
||||
ascending.value = !ascending.value
|
||||
} else {
|
||||
@@ -640,7 +621,7 @@ const updateAll = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const paths = await update_all(props.instance.path).catch(handleError)
|
||||
const paths = (await update_all(props.instance.path).catch(handleError)) as Record<string, string>
|
||||
|
||||
for (const [oldVal, newVal] of Object.entries(paths)) {
|
||||
const index = projects.value.findIndex((x) => x.path === oldVal)
|
||||
@@ -649,7 +630,7 @@ const updateAll = async () => {
|
||||
|
||||
if (projects.value[index].updateVersion) {
|
||||
projects.value[index].version = projects.value[index].updateVersion.version_number
|
||||
projects.value[index].updateVersion = null
|
||||
projects.value[index].updateVersion = undefined
|
||||
}
|
||||
}
|
||||
for (const project of setProjects) {
|
||||
@@ -664,15 +645,15 @@ const updateAll = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const updateProject = async (mod) => {
|
||||
const updateProject = async (mod: ProjectListEntry) => {
|
||||
mod.updating = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
|
||||
mod.updating = false
|
||||
|
||||
mod.outdated = false
|
||||
mod.version = mod.updateVersion.version_number
|
||||
mod.updateVersion = null
|
||||
mod.version = mod.updateVersion?.version_number
|
||||
mod.updateVersion = undefined
|
||||
|
||||
trackEvent('InstanceProjectUpdate', {
|
||||
loader: props.instance.loader,
|
||||
@@ -683,15 +664,15 @@ const updateProject = async (mod) => {
|
||||
})
|
||||
}
|
||||
|
||||
const locks = {}
|
||||
const locks: Record<string, string | null> = {}
|
||||
|
||||
const toggleDisableMod = async (mod) => {
|
||||
const toggleDisableMod = async (mod: ProjectListEntry) => {
|
||||
// Use mod's id as the key for the lock. If mod doesn't have a unique id, replace `mod.id` with some unique property.
|
||||
const lock = locks[mod.file_name]
|
||||
|
||||
while (lock) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout((_) => resolve(), 100)
|
||||
setTimeout((value: unknown) => resolve(value), 100)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -716,20 +697,20 @@ const toggleDisableMod = async (mod) => {
|
||||
locks[mod.file_name] = null
|
||||
}
|
||||
|
||||
const removeMod = async (mod) => {
|
||||
const removeMod = async (mod: ContentItem<ProjectListEntry>) => {
|
||||
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
||||
|
||||
trackEvent('InstanceProjectRemove', {
|
||||
loader: props.instance.loader,
|
||||
game_version: props.instance.game_version,
|
||||
id: mod.id,
|
||||
name: mod.name,
|
||||
project_type: mod.project_type,
|
||||
id: mod.data.id,
|
||||
name: mod.data.name,
|
||||
project_type: mod.data.project_type,
|
||||
})
|
||||
}
|
||||
|
||||
const copyModLink = async (mod) => {
|
||||
const copyModLink = async (mod: ContentItem<ProjectListEntry>) => {
|
||||
await navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`,
|
||||
)
|
||||
@@ -744,15 +725,15 @@ const deleteSelected = async () => {
|
||||
}
|
||||
|
||||
const shareNames = async () => {
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
await shareModal.value?.show(functionValues.value.map((x) => x.name).join('\n'))
|
||||
}
|
||||
|
||||
const shareFileNames = async () => {
|
||||
await shareModal.value.show(functionValues.value.map((x) => x.file_name).join('\n'))
|
||||
await shareModal.value?.show(functionValues.value.map((x) => x.file_name).join('\n'))
|
||||
}
|
||||
|
||||
const shareUrls = async () => {
|
||||
await shareModal.value.show(
|
||||
await shareModal.value?.show(
|
||||
functionValues.value
|
||||
.filter((x) => x.slug)
|
||||
.map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`)
|
||||
@@ -761,7 +742,7 @@ const shareUrls = async () => {
|
||||
}
|
||||
|
||||
const shareMarkdown = async () => {
|
||||
await shareModal.value.show(
|
||||
await shareModal.value?.show(
|
||||
functionValues.value
|
||||
.map((x) => {
|
||||
if (x.slug) {
|
||||
@@ -826,15 +807,17 @@ const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
|
||||
await initProjects()
|
||||
})
|
||||
|
||||
const unlistenProfiles = await profile_listener(async (event) => {
|
||||
if (
|
||||
event.profile_path_id === props.instance.path &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing'
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
})
|
||||
const unlistenProfiles = await profile_listener(
|
||||
async (event: { event: string; profile_path_id: string }) => {
|
||||
if (
|
||||
event.profile_path_id === props.instance.path &&
|
||||
event.event === 'synced' &&
|
||||
props.instance.install_stage !== 'pack_installing'
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<FilterBar v-model="filters" :options="filterOptions" />
|
||||
<FilterBar v-model="filters" :options="filterOptions" show-all-options />
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<WorldItem
|
||||
v-for="world in filteredWorlds"
|
||||
@@ -86,6 +86,7 @@
|
||||
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
||||
"
|
||||
@delete="() => promptToRemoveWorld(world)"
|
||||
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,6 +152,7 @@ import {
|
||||
hasQuickPlaySupport,
|
||||
refreshWorlds,
|
||||
handleDefaultProfileUpdateEvent,
|
||||
showWorldInFolder,
|
||||
} from '@/helpers/worlds.ts'
|
||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||
@@ -225,6 +227,11 @@ const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||
await refreshAllWorlds()
|
||||
|
||||
async function refreshServer(address: string) {
|
||||
if (!serverData.value[address]) {
|
||||
serverData.value[address] = {
|
||||
refreshing: true,
|
||||
}
|
||||
}
|
||||
await refreshServerData(serverData.value[address], protocolVersion.value, address)
|
||||
}
|
||||
|
||||
@@ -263,9 +270,12 @@ async function addServer(server: ServerWorld) {
|
||||
async function editServer(server: ServerWorld) {
|
||||
const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index)
|
||||
if (index !== -1) {
|
||||
const oldServer = worlds.value[index] as ServerWorld
|
||||
worlds.value[index] = server
|
||||
sortWorlds(worlds.value)
|
||||
await refreshServer(server.address)
|
||||
if (oldServer.address !== server.address) {
|
||||
await refreshServer(server.address)
|
||||
}
|
||||
} else {
|
||||
handleError(`Error refreshing server, refreshing all worlds`)
|
||||
await refreshAllWorlds()
|
||||
@@ -349,26 +359,34 @@ const supportsQuickPlay = computed(() =>
|
||||
const filterOptions = computed(() => {
|
||||
const options: FilterBarOption[] = []
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'singleplayer')) {
|
||||
const hasServer = worlds.value.some((x) => x.type === 'server')
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'singleplayer') && hasServer) {
|
||||
options.push({
|
||||
id: 'singleplayer',
|
||||
message: messages.singleplayer,
|
||||
})
|
||||
}
|
||||
|
||||
if (worlds.value.some((x) => x.type === 'server')) {
|
||||
options.push({
|
||||
id: 'server',
|
||||
message: messages.server,
|
||||
})
|
||||
}
|
||||
|
||||
// add available filter if there's any offline ("unavailable") servers
|
||||
if (hasServer) {
|
||||
// add available filter if there's any offline ("unavailable") servers AND there's any singleplayer worlds or available servers
|
||||
if (
|
||||
worlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'server' &&
|
||||
!serverData.value[x.address]?.status &&
|
||||
!serverData.value[x.address]?.refreshing,
|
||||
) &&
|
||||
worlds.value.some(
|
||||
(x) =>
|
||||
x.type === 'singleplayer' ||
|
||||
(x.type === 'server' &&
|
||||
serverData.value[x.address]?.status &&
|
||||
!serverData.value[x.address]?.refreshing),
|
||||
)
|
||||
) {
|
||||
options.push({
|
||||
|
||||
@@ -155,7 +155,7 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
@@ -170,6 +170,7 @@ import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const themeStore = useTheming()
|
||||
|
||||
@@ -247,6 +248,9 @@ async function install(version) {
|
||||
installedVersion.value = version
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ export const useInstall = defineStore('installStore', {
|
||||
setInstallConfirmModal(ref) {
|
||||
this.installConfirmModal = ref
|
||||
},
|
||||
showInstallConfirmModal(project, version_id, onInstall) {
|
||||
this.installConfirmModal.show(project, version_id, onInstall)
|
||||
showInstallConfirmModal(project, version_id, onInstall, createInstanceCallback) {
|
||||
this.installConfirmModal.show(project, version_id, onInstall, createInstanceCallback)
|
||||
},
|
||||
setIncompatibilityWarningModal(ref) {
|
||||
this.incompatibilityWarningModal = ref
|
||||
@@ -41,7 +41,14 @@ export const useInstall = defineStore('installStore', {
|
||||
},
|
||||
})
|
||||
|
||||
export const install = async (projectId, versionId, instancePath, source, callback = () => {}) => {
|
||||
export const install = async (
|
||||
projectId,
|
||||
versionId,
|
||||
instancePath,
|
||||
source,
|
||||
callback = () => {},
|
||||
createInstanceCallback = () => {},
|
||||
) => {
|
||||
const project = await get_project(projectId, 'must_revalidate').catch(handleError)
|
||||
|
||||
if (project.project_type === 'modpack') {
|
||||
@@ -49,7 +56,13 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
||||
const packs = await list().catch(handleError)
|
||||
|
||||
if (packs.length === 0 || !packs.find((pack) => pack.linked_data?.project_id === project.id)) {
|
||||
await packInstall(project.id, version, project.title, project.icon_url).catch(handleError)
|
||||
await packInstall(
|
||||
project.id,
|
||||
version,
|
||||
project.title,
|
||||
project.icon_url,
|
||||
createInstanceCallback,
|
||||
).catch(handleError)
|
||||
|
||||
trackEvent('PackInstall', {
|
||||
id: project.id,
|
||||
@@ -61,7 +74,7 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
||||
callback(version)
|
||||
} else {
|
||||
const install = useInstall()
|
||||
install.showInstallConfirmModal(project, version, callback)
|
||||
install.showInstallConfirmModal(project, version, callback, createInstanceCallback)
|
||||
}
|
||||
} else {
|
||||
if (instancePath) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useTheming } from './theme'
|
||||
import { useTheming } from './theme.ts'
|
||||
import { useBreadcrumbs } from './breadcrumbs'
|
||||
import { useLoading } from './loading'
|
||||
import { useNotifications, handleError } from './notifications'
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useTheming = defineStore('themeStore', {
|
||||
state: () => ({
|
||||
themeOptions: ['dark', 'light', 'oled', 'system'],
|
||||
advancedRendering: true,
|
||||
selectedTheme: 'dark',
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: {},
|
||||
}),
|
||||
actions: {
|
||||
setThemeState(newTheme) {
|
||||
if (this.themeOptions.includes(newTheme)) this.selectedTheme = newTheme
|
||||
else console.warn('Selected theme is not present. Check themeOptions.')
|
||||
|
||||
this.setThemeClass()
|
||||
},
|
||||
setThemeClass() {
|
||||
for (const theme of this.themeOptions) {
|
||||
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
||||
}
|
||||
|
||||
let theme = this.selectedTheme
|
||||
if (this.selectedTheme === 'system') {
|
||||
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (darkThemeMq.matches) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(`${theme}-mode`)
|
||||
},
|
||||
},
|
||||
})
|
||||
70
apps/app-frontend/src/store/theme.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = {
|
||||
project_background: false,
|
||||
page_path: false,
|
||||
worlds_tab: false,
|
||||
worlds_in_home: true,
|
||||
}
|
||||
|
||||
export const THEME_OPTIONS = ['dark', 'light', 'oled', 'system'] as const
|
||||
|
||||
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
|
||||
export type FeatureFlags = Record<FeatureFlag, boolean>
|
||||
export type ColorTheme = (typeof THEME_OPTIONS)[number]
|
||||
|
||||
export type ThemeStore = {
|
||||
selectedTheme: ColorTheme
|
||||
advancedRendering: boolean
|
||||
toggleSidebar: boolean
|
||||
|
||||
devMode: boolean
|
||||
featureFlags: FeatureFlags
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME_STORE: ThemeStore = {
|
||||
selectedTheme: 'dark',
|
||||
advancedRendering: true,
|
||||
toggleSidebar: false,
|
||||
|
||||
devMode: false,
|
||||
featureFlags: DEFAULT_FEATURE_FLAGS,
|
||||
}
|
||||
|
||||
export const useTheming = defineStore('themeStore', {
|
||||
state: () => DEFAULT_THEME_STORE,
|
||||
actions: {
|
||||
setThemeState(newTheme: ColorTheme) {
|
||||
if (THEME_OPTIONS.includes(newTheme)) {
|
||||
this.selectedTheme = newTheme
|
||||
} else {
|
||||
console.warn('Selected theme is not present. Check themeOptions.')
|
||||
}
|
||||
|
||||
this.setThemeClass()
|
||||
},
|
||||
setThemeClass() {
|
||||
for (const theme of THEME_OPTIONS) {
|
||||
document.getElementsByTagName('html')[0].classList.remove(`${theme}-mode`)
|
||||
}
|
||||
|
||||
let theme = this.selectedTheme
|
||||
if (this.selectedTheme === 'system') {
|
||||
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
if (darkThemeMq.matches) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementsByTagName('html')[0].classList.add(`${theme}-mode`)
|
||||
},
|
||||
getFeatureFlag(key: FeatureFlag) {
|
||||
return this.featureFlags[key] ?? DEFAULT_FEATURE_FLAGS[key]
|
||||
},
|
||||
getThemeOptions() {
|
||||
return THEME_OPTIONS
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,13 +1,14 @@
|
||||
[package]
|
||||
name = "theseus_playground"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
theseus = { path = "../../packages/app-lib", features = ["cli"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
webbrowser = "0.8.13"
|
||||
theseus = { workspace = true, features = ["cli"] }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
enumset.workspace = true
|
||||
|
||||
tracing = "0.1.37"
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"name": "@modrinth/app-playground",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use enumset::EnumSet;
|
||||
use theseus::prelude::*;
|
||||
use theseus::worlds::get_recent_worlds;
|
||||
|
||||
@@ -14,8 +15,7 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||
println!("A browser window will now open, follow the login flow there.");
|
||||
let login = minecraft_auth::begin_login().await?;
|
||||
|
||||
println!("URL {}", login.redirect_uri.as_str());
|
||||
webbrowser::open(login.redirect_uri.as_str())?;
|
||||
println!("Open URL {} in a browser", login.redirect_uri.as_str());
|
||||
|
||||
println!("Please enter URL code: ");
|
||||
let mut input = String::new();
|
||||
@@ -40,7 +40,7 @@ async fn main() -> theseus::Result<()> {
|
||||
// Initialize state
|
||||
State::init().await?;
|
||||
|
||||
let worlds = get_recent_worlds(4).await?;
|
||||
let worlds = get_recent_worlds(4, EnumSet::all()).await?;
|
||||
for world in worlds {
|
||||
println!(
|
||||
"World: {:?}/{:?} played at {:?}: {:#?}",
|
||||
|
||||
@@ -1,62 +1,51 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.9.4"
|
||||
version = "0.9.5"
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
edition.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.3", features = ["codegen"] }
|
||||
tauri-build = { workspace = true, features = ["codegen"] }
|
||||
|
||||
[dependencies]
|
||||
theseus = { path = "../../packages/app-lib", features = ["tauri"] }
|
||||
theseus = { workspace = true, features = ["tauri"] }
|
||||
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_with = "3.0.0"
|
||||
serde_json.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_with.workspace = true
|
||||
|
||||
tauri = { version = "2.1.1", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||
tauri-plugin-window-state = "2.2.0"
|
||||
tauri-plugin-deep-link = "2.2.0"
|
||||
tauri-plugin-os = "2.2.0"
|
||||
tauri-plugin-opener = "2.2.1"
|
||||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-updater = { version = "2.3.0" }
|
||||
tauri-plugin-single-instance = { version = "2.2.0" }
|
||||
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset"] }
|
||||
tauri-plugin-window-state.workspace = true
|
||||
tauri-plugin-deep-link.workspace = true
|
||||
tauri-plugin-os.workspace = true
|
||||
tauri-plugin-opener.workspace = true
|
||||
tauri-plugin-dialog.workspace = true
|
||||
tauri-plugin-updater.workspace = true
|
||||
tauri-plugin-single-instance.workspace = true
|
||||
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
thiserror = "1.0"
|
||||
daedalus = { path = "../../packages/daedalus" }
|
||||
chrono = "0.4.26"
|
||||
either = "1.15"
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
thiserror.workspace = true
|
||||
daedalus.workspace = true
|
||||
chrono.workspace = true
|
||||
either.workspace = true
|
||||
|
||||
url = "2.2"
|
||||
urlencoding = "2.1"
|
||||
uuid = { version = "1.1", features = ["serde", "v4"] }
|
||||
os_info = "3.7.0"
|
||||
url.workspace = true
|
||||
urlencoding.workspace = true
|
||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||
|
||||
tracing = "0.1.37"
|
||||
tracing-error = "0.2.0"
|
||||
tracing.workspace = true
|
||||
tracing-error.workspace = true
|
||||
|
||||
dashmap = "6.0.1"
|
||||
paste = "1.0.15"
|
||||
dashmap.workspace = true
|
||||
paste.workspace = true
|
||||
enumset = { workspace = true, features = ["serde"] }
|
||||
|
||||
opener = { version = "0.7.2", features = ["reveal", "dbus-vendored"] }
|
||||
|
||||
native-dialog = "0.7.0"
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
window-shadows = "0.2.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.25.0"
|
||||
objc = "0.2.7"
|
||||
rand = "0.8.5"
|
||||
native-dialog.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tauri-plugin-updater = { version = "2.3.0", optional = true, features = ["native-tls-vendored", "zip"], default-features = false }
|
||||
tauri-plugin-updater = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
@@ -66,3 +55,6 @@ default = ["custom-protocol"]
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
updater = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -248,6 +248,7 @@ fn main() {
|
||||
"get_recent_worlds",
|
||||
"get_profile_worlds",
|
||||
"get_singleplayer_world",
|
||||
"set_world_display_status",
|
||||
"rename_world",
|
||||
"reset_world_icon",
|
||||
"backup_world",
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 59 KiB |
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@modrinth/app",
|
||||
"scripts": {
|
||||
"build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev",
|
||||
"test": "cargo test",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.1.0"
|
||||
"@tauri-apps/cli": "2.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/app-frontend": "workspace:*",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::Runtime;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use theseus::{
|
||||
handler,
|
||||
prelude::{CommandPayload, DirectoryInfo},
|
||||
@@ -35,6 +37,7 @@ pub fn get_os() -> OS {
|
||||
let os = OS::MacOS;
|
||||
os
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum OS {
|
||||
@@ -47,57 +50,56 @@ pub enum OS {
|
||||
// Create a new HashMap with the same keys
|
||||
// Values provided should not be used directly, as they are not guaranteed to be up-to-date
|
||||
#[tauri::command]
|
||||
pub async fn progress_bars_list(
|
||||
) -> Result<DashMap<uuid::Uuid, theseus::LoadingBar>> {
|
||||
pub async fn progress_bars_list()
|
||||
-> Result<DashMap<uuid::Uuid, theseus::LoadingBar>> {
|
||||
let res = theseus::EventState::list_progress_bars().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// cfg only on mac os
|
||||
// disables mouseover and fixes a random crash error only fixed by recent versions of macos
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tauri::command]
|
||||
pub async fn should_disable_mouseover() -> bool {
|
||||
// We try to match version to 12.2 or higher. If unrecognizable to pattern or lower, we default to the css with disabled mouseover for safety
|
||||
let os = os_info::get();
|
||||
if let os_info::Version::Semantic(major, minor, _) = os.version() {
|
||||
if *major >= 12 && *minor >= 3 {
|
||||
// Mac os version is 12.3 or higher, we allow mouseover
|
||||
return false;
|
||||
if cfg!(target_os = "macos") {
|
||||
// We try to match version to 12.2 or higher. If unrecognizable to pattern or lower, we default to the css with disabled mouseover for safety
|
||||
if let tauri_plugin_os::Version::Semantic(major, minor, _) =
|
||||
tauri_plugin_os::version()
|
||||
{
|
||||
if major >= 12 && minor >= 3 {
|
||||
// Mac os version is 12.3 or higher, we allow mouseover
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
// Not macos, we allow mouseover
|
||||
false
|
||||
}
|
||||
true
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[tauri::command]
|
||||
pub async fn should_disable_mouseover() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn highlight_in_folder(path: PathBuf) {
|
||||
let res = opener::reveal(path);
|
||||
|
||||
if let Err(e) = res {
|
||||
pub fn highlight_in_folder<R: Runtime>(
|
||||
app: tauri::AppHandle<R>,
|
||||
path: PathBuf,
|
||||
) {
|
||||
if let Err(e) = app.opener().reveal_item_in_dir(path) {
|
||||
tracing::error!("Failed to highlight file in folder: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_path(path: PathBuf) {
|
||||
let res = opener::open(path);
|
||||
|
||||
if let Err(e) = res {
|
||||
pub fn open_path<R: Runtime>(app: tauri::AppHandle<R>, path: PathBuf) {
|
||||
if let Err(e) = app.opener().open_path(path.to_string_lossy(), None::<&str>)
|
||||
{
|
||||
tracing::error!("Failed to open path: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn show_launcher_logs_folder() {
|
||||
pub fn show_launcher_logs_folder<R: Runtime>(app: tauri::AppHandle<R>) {
|
||||
let path = DirectoryInfo::launcher_logs_dir().unwrap_or_default();
|
||||
// failure to get folder just opens filesystem
|
||||
// (ie: if in debug mode only and launcher_logs never created)
|
||||
open_path(path);
|
||||
open_path(app, path);
|
||||
}
|
||||
|
||||
// Get opening command
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::api::Result;
|
||||
use either::Either;
|
||||
use enumset::EnumSet;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use theseus::prelude::ProcessMetadata;
|
||||
use theseus::profile::{get_full_path, QuickPlayType};
|
||||
use theseus::profile::{QuickPlayType, get_full_path};
|
||||
use theseus::worlds::{
|
||||
ServerPackStatus, ServerStatus, World, WorldWithProfile,
|
||||
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
||||
WorldWithProfile,
|
||||
};
|
||||
use theseus::{profile, worlds};
|
||||
|
||||
@@ -14,6 +16,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
get_recent_worlds,
|
||||
get_profile_worlds,
|
||||
get_singleplayer_world,
|
||||
set_world_display_status,
|
||||
rename_world,
|
||||
reset_world_icon,
|
||||
backup_world,
|
||||
@@ -33,9 +36,14 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||
pub async fn get_recent_worlds<R: Runtime>(
|
||||
app_handle: AppHandle<R>,
|
||||
limit: usize,
|
||||
display_statuses: Option<EnumSet<DisplayStatus>>,
|
||||
) -> Result<Vec<WorldWithProfile>> {
|
||||
let mut result = worlds::get_recent_worlds(limit).await?;
|
||||
for world in result.iter_mut() {
|
||||
let mut result = worlds::get_recent_worlds(
|
||||
limit,
|
||||
display_statuses.unwrap_or(EnumSet::all()),
|
||||
)
|
||||
.await?;
|
||||
for world in &mut result {
|
||||
adapt_world_icon(&app_handle, &mut world.world);
|
||||
}
|
||||
Ok(result)
|
||||
@@ -47,7 +55,7 @@ pub async fn get_profile_worlds<R: Runtime>(
|
||||
path: &str,
|
||||
) -> Result<Vec<World>> {
|
||||
let mut result = worlds::get_profile_worlds(path).await?;
|
||||
for world in result.iter_mut() {
|
||||
for world in &mut result {
|
||||
adapt_world_icon(&app_handle, world);
|
||||
}
|
||||
Ok(result)
|
||||
@@ -59,8 +67,7 @@ pub async fn get_singleplayer_world<R: Runtime>(
|
||||
instance: &str,
|
||||
world: &str,
|
||||
) -> Result<World> {
|
||||
let instance = get_full_path(instance).await?;
|
||||
let mut world = worlds::get_singleplayer_world(&instance, world).await?;
|
||||
let mut world = worlds::get_singleplayer_world(instance, world).await?;
|
||||
adapt_world_icon(&app_handle, &mut world);
|
||||
Ok(world)
|
||||
}
|
||||
@@ -90,6 +97,22 @@ fn adapt_world_icon<R: Runtime>(app_handle: &AppHandle<R>, world: &mut World) {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn set_world_display_status(
|
||||
instance: &str,
|
||||
world_type: WorldType,
|
||||
world_id: &str,
|
||||
display_status: DisplayStatus,
|
||||
) -> Result<()> {
|
||||
Ok(worlds::set_world_display_status(
|
||||
instance,
|
||||
world_type,
|
||||
world_id,
|
||||
display_status,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn rename_world(
|
||||
instance: &str,
|
||||
|
||||
@@ -11,7 +11,8 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
||||
manager: &M,
|
||||
) -> InitialPayload {
|
||||
let initial_payload = manager.try_state::<InitialPayload>();
|
||||
let mtx = if let Some(initial_payload) = initial_payload {
|
||||
|
||||
if let Some(initial_payload) = initial_payload {
|
||||
initial_payload.inner().clone()
|
||||
} else {
|
||||
tracing::info!("No initial payload found, creating new");
|
||||
@@ -22,7 +23,5 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
||||
manager.manage(payload.clone());
|
||||
|
||||
payload
|
||||
};
|
||||
|
||||
mtx
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
pub mod deep_link;
|
||||
pub mod window_ext;
|
||||
|
||||
@@ -1,412 +0,0 @@
|
||||
// Stolen from https://gist.github.com/charrondev/43150e940bd2771b1ea88256d491c7a9
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Emitter, Runtime, Window,
|
||||
}; // 0.8
|
||||
|
||||
const WINDOW_CONTROL_PAD_X: f64 = 9.0;
|
||||
const WINDOW_CONTROL_PAD_Y: f64 = 10.0;
|
||||
|
||||
struct UnsafeWindowHandle(*mut std::ffi::c_void);
|
||||
unsafe impl Send for UnsafeWindowHandle {}
|
||||
unsafe impl Sync for UnsafeWindowHandle {}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("traffic_light_positioner")
|
||||
.on_window_ready(|window| {
|
||||
#[cfg(target_os = "macos")]
|
||||
setup_traffic_light_positioner(window);
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn position_traffic_lights(
|
||||
ns_window_handle: UnsafeWindowHandle,
|
||||
x: f64,
|
||||
y: f64,
|
||||
) {
|
||||
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
|
||||
use cocoa::foundation::NSRect;
|
||||
let ns_window = ns_window_handle.0 as cocoa::base::id;
|
||||
unsafe {
|
||||
let close = ns_window
|
||||
.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
|
||||
let miniaturize = ns_window
|
||||
.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
||||
let zoom =
|
||||
ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
||||
|
||||
let title_bar_container_view = close.superview().superview();
|
||||
|
||||
let close_rect: NSRect = msg_send![close, frame];
|
||||
let button_height = close_rect.size.height + 12.0;
|
||||
|
||||
let title_bar_frame_height = button_height + y;
|
||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||
title_bar_rect.size.height = title_bar_frame_height;
|
||||
title_bar_rect.origin.y =
|
||||
NSView::frame(ns_window).size.height - title_bar_frame_height;
|
||||
let _: () =
|
||||
msg_send![title_bar_container_view, setFrame: title_bar_rect];
|
||||
|
||||
let window_buttons = vec![close, miniaturize, zoom];
|
||||
let space_between =
|
||||
NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
|
||||
|
||||
for (i, button) in window_buttons.into_iter().enumerate() {
|
||||
let mut rect: NSRect = NSView::frame(button);
|
||||
rect.origin.x = x + (i as f64 * space_between) + 6.0;
|
||||
button.setFrameOrigin(rect.origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Debug)]
|
||||
struct WindowState<R: Runtime> {
|
||||
window: Window<R>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
|
||||
use cocoa::appkit::NSWindow;
|
||||
use cocoa::base::{id, BOOL};
|
||||
use cocoa::foundation::NSUInteger;
|
||||
use objc::runtime::{Object, Sel};
|
||||
use std::ffi::c_void;
|
||||
|
||||
// Do the initial positioning
|
||||
position_traffic_lights(
|
||||
UnsafeWindowHandle(
|
||||
window.ns_window().expect("Failed to create window handle"),
|
||||
),
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
|
||||
// Ensure they stay in place while resizing the window.
|
||||
fn with_window_state<R: Runtime, F: FnOnce(&mut WindowState<R>) -> T, T>(
|
||||
this: &Object,
|
||||
func: F,
|
||||
) {
|
||||
let ptr = unsafe {
|
||||
let x: *mut c_void = *this.get_ivar("app_box");
|
||||
&mut *(x as *mut WindowState<R>)
|
||||
};
|
||||
func(ptr);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ns_win = window
|
||||
.ns_window()
|
||||
.expect("NS Window should exist to mount traffic light delegate.")
|
||||
as id;
|
||||
|
||||
let current_delegate: id = ns_win.delegate();
|
||||
|
||||
extern "C" fn on_window_should_close(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
sender: id,
|
||||
) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, windowShouldClose: sender]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_close(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillClose: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_resize<R: Runtime>(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
let id = state.window.ns_window().expect(
|
||||
"NS window should exist on state to handle resize",
|
||||
) as id;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
position_traffic_lights(
|
||||
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidResize: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_move(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidMove: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_change_backing_properties(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_become_key(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () =
|
||||
msg_send![super_del, windowDidBecomeKey: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_resign_key(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () =
|
||||
msg_send![super_del, windowDidResignKey: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_dragging_entered(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, draggingEntered: notification]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_prepare_for_drag_operation(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, prepareForDragOperation: notification]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_perform_drag_operation(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
sender: id,
|
||||
) -> BOOL {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, performDragOperation: sender]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_conclude_drag_operation(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () =
|
||||
msg_send![super_del, concludeDragOperation: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_dragging_exited(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, draggingExited: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_use_full_screen_presentation_options(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
window: id,
|
||||
proposed_options: NSUInteger,
|
||||
) -> NSUInteger {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_enter_full_screen<R: Runtime>(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("did-enter-fullscreen", ())
|
||||
.expect("Failed to emit event");
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_enter_full_screen<R: Runtime>(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("will-enter-fullscreen", ())
|
||||
.expect("Failed to emit event");
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_exit_full_screen<R: Runtime>(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("did-exit-fullscreen", ())
|
||||
.expect("Failed to emit event");
|
||||
|
||||
let id =
|
||||
state.window.ns_window().expect("Failed to emit event")
|
||||
as id;
|
||||
position_traffic_lights(
|
||||
UnsafeWindowHandle(id as *mut std::ffi::c_void),
|
||||
WINDOW_CONTROL_PAD_X,
|
||||
WINDOW_CONTROL_PAD_Y,
|
||||
);
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () =
|
||||
msg_send![super_del, windowDidExitFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_will_exit_full_screen<R: Runtime>(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
with_window_state(this, |state: &mut WindowState<R>| {
|
||||
state
|
||||
.window
|
||||
.emit("will-exit-fullscreen", ())
|
||||
.expect("Failed to emit event");
|
||||
});
|
||||
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_window_did_fail_to_enter_full_screen(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
window: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_effective_appearance_did_change(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
|
||||
}
|
||||
}
|
||||
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
|
||||
this: &Object,
|
||||
_cmd: Sel,
|
||||
notification: id,
|
||||
) {
|
||||
unsafe {
|
||||
let super_del: id = *this.get_ivar("super_delegate");
|
||||
let _: () = msg_send![
|
||||
super_del,
|
||||
effectiveAppearanceDidChangedOnMainThread: notification
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Are we deallocing this properly ? (I miss safe Rust :( )
|
||||
let window_label = window.label().to_string();
|
||||
|
||||
let app_state = WindowState { window };
|
||||
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
|
||||
let random_str: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(20)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
// We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate
|
||||
// delegate with the same name.
|
||||
let delegate_name =
|
||||
format!("windowDelegate_{}_{}", window_label, random_str);
|
||||
|
||||
ns_win.setDelegate_(delegate!(&delegate_name, {
|
||||
window: id = ns_win,
|
||||
app_box: *mut c_void = app_box,
|
||||
toolbar: id = cocoa::base::nil,
|
||||
super_delegate: id = current_delegate,
|
||||
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
|
||||
(windowDidResize:) => on_window_did_resize::<R> as extern fn(&Object, Sel, id),
|
||||
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
|
||||
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
|
||||
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
|
||||
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
|
||||
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
|
||||
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
|
||||
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
|
||||
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
|
||||
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowDidExitFullScreen:) => on_window_did_exit_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowWillExitFullScreen:) => on_window_will_exit_full_screen::<R> as extern fn(&Object, Sel, id),
|
||||
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
|
||||
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use native_dialog::{MessageDialog, MessageType};
|
||||
use native_dialog::{DialogBuilder, MessageLevel};
|
||||
use std::env;
|
||||
use tauri::{Listener, Manager};
|
||||
use theseus::prelude::*;
|
||||
@@ -14,14 +14,6 @@ mod error;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate cocoa;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
// Should be called in launcher initialization
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[tauri::command]
|
||||
@@ -113,14 +105,14 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||
fn show_window(app: tauri::AppHandle) {
|
||||
let win = app.get_window("main").unwrap();
|
||||
if let Err(e) = win.show() {
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
DialogBuilder::message()
|
||||
.set_level(MessageLevel::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text(&format!(
|
||||
"Cannot display application window due to an error:\n{}",
|
||||
e
|
||||
.set_text(format!(
|
||||
"Cannot display application window due to an error:\n{e}"
|
||||
))
|
||||
.show_alert()
|
||||
.alert()
|
||||
.show()
|
||||
.unwrap();
|
||||
panic!("cannot display application window")
|
||||
} else {
|
||||
@@ -138,8 +130,7 @@ fn is_dev() -> bool {
|
||||
async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
|
||||
window.set_decorations(b).map_err(|e| {
|
||||
theseus::Error::from(theseus::ErrorKind::OtherError(format!(
|
||||
"Failed to toggle decorations: {}",
|
||||
e
|
||||
"Failed to toggle decorations: {e}"
|
||||
)))
|
||||
})?;
|
||||
Ok(())
|
||||
@@ -206,7 +197,7 @@ fn main() {
|
||||
{
|
||||
let payload = macos::deep_link::get_or_init_payload(app);
|
||||
|
||||
let mtx_copy = payload.payload.clone();
|
||||
let mtx_copy = payload.payload;
|
||||
app.listen("deep-link://new-url", move |url| {
|
||||
let mtx_copy_copy = mtx_copy.clone();
|
||||
let request = url.payload().to_owned();
|
||||
@@ -238,13 +229,12 @@ fn main() {
|
||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||
payload,
|
||||
));
|
||||
dbg!(url);
|
||||
});
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
if let Some(window) = app.get_window("main") {
|
||||
window.set_shadow(true).unwrap();
|
||||
if let Some(window) = app.get_window("main") {
|
||||
if let Err(e) = window.set_shadow(true) {
|
||||
tracing::warn!("Failed to set window shadow: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,32 +267,27 @@ fn main() {
|
||||
restart_app,
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.plugin(macos::window_ext::init());
|
||||
}
|
||||
|
||||
tracing::info!("Initializing app...");
|
||||
let app = builder.build(tauri::generate_context!());
|
||||
|
||||
match app {
|
||||
Ok(app) => {
|
||||
#[allow(unused_variables)]
|
||||
app.run(|app, event| {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
drop((app, event));
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Opened { urls } = event {
|
||||
tracing::info!("Handling webview open {urls:?}");
|
||||
|
||||
let file = urls
|
||||
.into_iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
.next();
|
||||
.find_map(|url| url.to_file_path().ok());
|
||||
|
||||
if let Some(file) = file {
|
||||
let payload =
|
||||
macos::deep_link::get_or_init_payload(app);
|
||||
|
||||
let mtx_copy = payload.payload.clone();
|
||||
let mtx_copy = payload.payload;
|
||||
let request = file.to_string_lossy().to_string();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let mut payload = mtx_copy.lock().await;
|
||||
@@ -320,28 +305,29 @@ fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
|
||||
if format!("{:?}", e).contains(
|
||||
if format!("{e:?}").contains(
|
||||
"Runtime(CreateWebview(WebView2Error(WindowsError",
|
||||
) {
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
DialogBuilder::message()
|
||||
.set_level(MessageLevel::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text("Your Microsoft Edge WebView2 installation is corrupt.\n\nMicrosoft Edge WebView2 is required to run Modrinth App.\n\nLearn how to repair it at https://support.modrinth.com/en/articles/8797765-corrupted-microsoft-edge-webview2-installation")
|
||||
.show_alert()
|
||||
.alert()
|
||||
.show()
|
||||
.unwrap();
|
||||
|
||||
panic!("webview2 initialization failed")
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
DialogBuilder::message()
|
||||
.set_level(MessageLevel::Error)
|
||||
.set_title("Initialization error")
|
||||
.set_text(&format!(
|
||||
"Cannot initialize application due to an error:\n{:?}",
|
||||
e
|
||||
.set_text(format!(
|
||||
"Cannot initialize application due to an error:\n{e:?}"
|
||||
))
|
||||
.show_alert()
|
||||
.alert()
|
||||
.show()
|
||||
.unwrap();
|
||||
|
||||
tracing::error!("Error while running tauri application: {:?}", e);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm turbo run dev --filter=@modrinth/app-frontend",
|
||||
"beforeBuildCommand": "pnpm turbo run build --filter=@modrinth/app-frontend",
|
||||
@@ -44,7 +45,7 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.9.4",
|
||||
"version": "0.9.5",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
@@ -76,18 +77,26 @@
|
||||
],
|
||||
"security": {
|
||||
"assetProtocol": {
|
||||
"scope": ["$APPDATA/caches/icons/*", "$APPCONFIG/caches/icons/*", "$CONFIG/caches/icons/*", "$APPDATA/profiles/*/saves/*/icon.png", "$APPCONFIG/profiles/*/saves/*/icon.png", "$CONFIG/profiles/*/saves/*/icon.png"],
|
||||
"scope": [
|
||||
"$APPDATA/caches/icons/*",
|
||||
"$APPCONFIG/caches/icons/*",
|
||||
"$CONFIG/caches/icons/*",
|
||||
"$APPDATA/profiles/*/saves/*/icon.png",
|
||||
"$APPCONFIG/profiles/*/saves/*/icon.png",
|
||||
"$CONFIG/profiles/*/saves/*/icon.png"
|
||||
],
|
||||
"enable": true
|
||||
},
|
||||
"capabilities": ["ads", "core", "plugins"],
|
||||
"csp": {
|
||||
"default-src": "'self' customprotocol: asset:",
|
||||
"connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs",
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/inter/"],
|
||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self'",
|
||||
"script-src": "https://*.posthog.com 'self'",
|
||||
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'"
|
||||
"frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com 'self'",
|
||||
"media-src": "https://*.githubusercontent.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
"minWidth": 1100,
|
||||
"visible": false,
|
||||
"zoomHotkeysEnabled": false,
|
||||
"decorations": true
|
||||
"decorations": true,
|
||||
"trafficLightPosition": {
|
||||
"x": 15.0,
|
||||
"y": 22.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
14
apps/app/turbo.jsonc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "../../node_modules/turbo/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
// Running Clippy and tests on a Tauri application requires
|
||||
// the frontend to be built at least once first
|
||||
"lint": {
|
||||
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["@modrinth/app-frontend#build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,39 +2,32 @@
|
||||
name = "daedalus_client"
|
||||
version = "0.2.2"
|
||||
authors = ["Jai A <jai@modrinth.com>"]
|
||||
edition = "2021"
|
||||
edition.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
daedalus = { path = "../../packages/daedalus" }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures = "0.3.25"
|
||||
dotenvy = "0.15.6"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde-xml-rs = "0.6.0"
|
||||
lazy_static = "1.4.0"
|
||||
thiserror = "1.0"
|
||||
reqwest = { version = "0.12.5", default-features = false, features = [
|
||||
"stream",
|
||||
"json",
|
||||
"rustls-tls-native-roots",
|
||||
] }
|
||||
async_zip = { version = "0.0.17", features = ["full"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
bytes = "1.6.0"
|
||||
rust-s3 = { version = "0.33.0", default-features = false, features = [
|
||||
"fail-on-err",
|
||||
"tags",
|
||||
"tokio-rustls-tls",
|
||||
"reqwest",
|
||||
] }
|
||||
dashmap = "5.5.3"
|
||||
sha1_smol = { version = "1.0.0", features = ["std"] }
|
||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||
itertools = "0.13.0"
|
||||
tracing-error = "0.2.0"
|
||||
daedalus.workspace = true
|
||||
tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] }
|
||||
futures.workspace = true
|
||||
dotenvy.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
serde-xml-rs.workspace = true
|
||||
thiserror.workspace = true
|
||||
reqwest = { workspace = true, features = ["stream", "json", "rustls-tls-native-roots"] }
|
||||
async_zip = { workspace = true, features = ["chrono", "tokio-fs", "deflate", "bzip2", "zstd", "deflate64"] }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
bytes.workspace = true
|
||||
rust-s3.workspace = true
|
||||
dashmap.workspace = true
|
||||
sha1_smol.workspace = true
|
||||
indexmap = { workspace = true, features = ["serde"] }
|
||||
itertools.workspace = true
|
||||
tracing-error.workspace = true
|
||||
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM rust:1.82.0 as build
|
||||
FROM rust:1.87.0 AS build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/daedalus
|
||||
@@ -9,9 +9,9 @@ RUN cargo build --release --package daedalus_client
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"name": "@modrinth/daedalus_client",
|
||||
"scripts": {
|
||||
"build": "cargo build --release",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
||||
"fix": "cargo fmt && cargo clippy --fix",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||
"dev": "cargo run",
|
||||
"test": "cargo test"
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/daedalus": "workspace:*"
|
||||
|
||||
@@ -12,7 +12,9 @@ pub enum ErrorKind {
|
||||
SerdeJSON(#[from] serde_json::Error),
|
||||
#[error("Error while deserializing XML: {0}")]
|
||||
SerdeXML(#[from] serde_xml_rs::Error),
|
||||
#[error("Failed to validate file checksum at url {url} with hash {hash} after {tries} tries")]
|
||||
#[error(
|
||||
"Failed to validate file checksum at url {url} with hash {hash} after {tries} tries"
|
||||
)]
|
||||
ChecksumFailure {
|
||||
hash: String,
|
||||
url: String,
|
||||
@@ -22,7 +24,7 @@ pub enum ErrorKind {
|
||||
Fetch { inner: reqwest::Error, item: String },
|
||||
#[error("Error while uploading file to S3: {file}")]
|
||||
S3 {
|
||||
inner: s3::error::S3Error,
|
||||
inner: Box<s3::error::S3Error>,
|
||||
file: String,
|
||||
},
|
||||
#[error("Error acquiring semaphore: {0}")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::util::{download_file, fetch_json, format_url};
|
||||
use crate::{insert_mirrored_artifact, Error, MirrorArtifact, UploadFile};
|
||||
use daedalus::modded::{Manifest, PartialVersionInfo, DUMMY_REPLACE_STRING};
|
||||
use crate::{Error, MirrorArtifact, UploadFile, insert_mirrored_artifact};
|
||||
use daedalus::modded::{DUMMY_REPLACE_STRING, Manifest, PartialVersionInfo};
|
||||
use dashmap::DashMap;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
@@ -169,10 +169,11 @@ async fn fetch(
|
||||
insert_mirrored_artifact(
|
||||
&new_name,
|
||||
None,
|
||||
vec![lib
|
||||
.url
|
||||
.clone()
|
||||
.unwrap_or_else(|| maven_url.to_string())],
|
||||
vec![
|
||||
lib.url
|
||||
.clone()
|
||||
.unwrap_or_else(|| maven_url.to_string()),
|
||||
],
|
||||
false,
|
||||
mirror_artifacts,
|
||||
)?;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::util::{download_file, fetch_json, fetch_xml, format_url};
|
||||
use crate::{insert_mirrored_artifact, Error, MirrorArtifact, UploadFile};
|
||||
use crate::{Error, MirrorArtifact, UploadFile, insert_mirrored_artifact};
|
||||
use chrono::{DateTime, Utc};
|
||||
use daedalus::get_path_from_artifact;
|
||||
use daedalus::modded::PartialVersionInfo;
|
||||
@@ -7,8 +7,8 @@ use dashmap::DashMap;
|
||||
use futures::io::Cursor;
|
||||
use indexmap::IndexMap;
|
||||
use itertools::Itertools;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
@@ -57,7 +57,7 @@ pub async fn fetch_forge(
|
||||
|
||||
ForgeVersion {
|
||||
format_version,
|
||||
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.minecraftforge.net/net/minecraftforge/forge/{loader_version}/forge-{loader_version}-installer.jar"),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: game_version.clone(),
|
||||
@@ -137,7 +137,7 @@ pub async fn fetch_neo(
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{0}/forge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/forge/{loader_version}/forge-{loader_version}-installer.jar"),
|
||||
raw: loader_version,
|
||||
loader_version: version_split,
|
||||
game_version: "1.20.1".to_string(), // All NeoForge Forge versions are for 1.20.1
|
||||
@@ -163,7 +163,7 @@ pub async fn fetch_neo(
|
||||
|
||||
Ok(ForgeVersion {
|
||||
format_version: 2,
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{0}/neoforge-{0}-installer.jar", loader_version),
|
||||
installer_url: format!("https://maven.neoforged.net/net/neoforged/neoforge/{loader_version}/neoforge-{loader_version}-installer.jar"),
|
||||
loader_version: loader_version.clone(),
|
||||
raw: loader_version,
|
||||
game_version,
|
||||
@@ -502,7 +502,7 @@ async fn fetch(
|
||||
)?;
|
||||
|
||||
artifact.url =
|
||||
format_url(&format!("maven/{}", artifact_path));
|
||||
format_url(&format!("maven/{artifact_path}"));
|
||||
|
||||
return Ok(lib);
|
||||
}
|
||||
@@ -589,14 +589,16 @@ async fn fetch(
|
||||
mod_loader: &str,
|
||||
version: &ForgeVersion,
|
||||
) -> Result<String, Error> {
|
||||
let extract_file =
|
||||
read_file(zip, &value[1..value.len()])
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
let extract_file = read_file(
|
||||
zip,
|
||||
&value[1..value.len()],
|
||||
)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::InvalidInput(format!(
|
||||
"Unable reading data key {key} at path {value}",
|
||||
))
|
||||
})?;
|
||||
})?;
|
||||
|
||||
let file_name = value.split('/').next_back()
|
||||
.ok_or_else(|| {
|
||||
@@ -622,10 +624,7 @@ async fn fetch(
|
||||
|
||||
let path = format!(
|
||||
"com.modrinth.daedalus:{}-installer-extracts:{}:{}@{}",
|
||||
mod_loader,
|
||||
version.raw,
|
||||
file_name,
|
||||
ext
|
||||
mod_loader, version.raw, file_name, ext
|
||||
);
|
||||
|
||||
upload_files.insert(
|
||||
@@ -753,7 +752,8 @@ async fn fetch(
|
||||
.rev()
|
||||
.chunk_by(|x| x.game_version.clone())
|
||||
.into_iter()
|
||||
.map(|(game_version, loaders)| daedalus::modded::Version {
|
||||
.map(|(game_version, loaders)| {
|
||||
daedalus::modded::Version {
|
||||
id: game_version,
|
||||
stable: true,
|
||||
loaders: loaders
|
||||
@@ -766,6 +766,7 @@ async fn fetch(
|
||||
stable: false,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::util::{
|
||||
format_url, upload_file_to_bucket, upload_url_to_bucket_mirrors,
|
||||
REQWEST_CLIENT,
|
||||
REQWEST_CLIENT, format_url, upload_file_to_bucket,
|
||||
upload_url_to_bucket_mirrors,
|
||||
};
|
||||
use daedalus::get_path_from_artifact;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
||||
|
||||
mod error;
|
||||
mod fabric;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::util::fetch_json;
|
||||
use crate::{
|
||||
util::download_file, util::format_url, util::sha1_async, Error,
|
||||
MirrorArtifact, UploadFile,
|
||||
Error, MirrorArtifact, UploadFile, util::download_file, util::format_url,
|
||||
util::sha1_async,
|
||||
};
|
||||
use daedalus::minecraft::{
|
||||
merge_partial_library, Library, PartialLibrary, VersionInfo,
|
||||
VersionManifest, VERSION_MANIFEST_URL,
|
||||
Library, PartialLibrary, VERSION_MANIFEST_URL, VersionInfo,
|
||||
VersionManifest, merge_partial_library,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use serde::Deserialize;
|
||||
@@ -52,8 +52,7 @@ pub async fn fetch(
|
||||
if modrinth_version
|
||||
.original_sha1
|
||||
.as_ref()
|
||||
.map(|x| x == &version.sha1)
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|x| x == &version.sha1)
|
||||
{
|
||||
existing_versions.push(modrinth_version);
|
||||
} else {
|
||||
|
||||
@@ -3,59 +3,57 @@ use bytes::Bytes;
|
||||
use s3::creds::Credentials;
|
||||
use s3::{Bucket, Region};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref BUCKET : Bucket = {
|
||||
let region = dotenvy::var("S3_REGION").unwrap();
|
||||
let b = Bucket::new(
|
||||
&dotenvy::var("S3_BUCKET_NAME").unwrap(),
|
||||
if &*region == "r2" {
|
||||
Region::R2 {
|
||||
account_id: dotenvy::var("S3_URL").unwrap(),
|
||||
}
|
||||
} else {
|
||||
Region::Custom {
|
||||
region: region.clone(),
|
||||
endpoint: dotenvy::var("S3_URL").unwrap(),
|
||||
}
|
||||
},
|
||||
Credentials::new(
|
||||
Some(&*dotenvy::var("S3_ACCESS_TOKEN").unwrap()),
|
||||
Some(&*dotenvy::var("S3_SECRET").unwrap()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
).unwrap(),
|
||||
).unwrap();
|
||||
|
||||
if region == "path-style" {
|
||||
b.with_path_style()
|
||||
static BUCKET: LazyLock<Bucket> = LazyLock::new(|| {
|
||||
let region = dotenvy::var("S3_REGION").unwrap();
|
||||
let b = Bucket::new(
|
||||
&dotenvy::var("S3_BUCKET_NAME").unwrap(),
|
||||
if &*region == "r2" {
|
||||
Region::R2 {
|
||||
account_id: dotenvy::var("S3_URL").unwrap(),
|
||||
}
|
||||
} else {
|
||||
b
|
||||
}
|
||||
};
|
||||
}
|
||||
Region::Custom {
|
||||
region: region.clone(),
|
||||
endpoint: dotenvy::var("S3_URL").unwrap(),
|
||||
}
|
||||
},
|
||||
Credentials::new(
|
||||
Some(&*dotenvy::var("S3_ACCESS_TOKEN").unwrap()),
|
||||
Some(&*dotenvy::var("S3_SECRET").unwrap()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref REQWEST_CLIENT: reqwest::Client = {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
if let Ok(header) = reqwest::header::HeaderValue::from_str(&format!(
|
||||
"modrinth/daedalus/{} (support@modrinth.com)",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)) {
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
}
|
||||
if region == "path-style" {
|
||||
*b.with_path_style()
|
||||
} else {
|
||||
*b
|
||||
}
|
||||
});
|
||||
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(std::time::Duration::from_secs(10)))
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
if let Ok(header) = reqwest::header::HeaderValue::from_str(&format!(
|
||||
"modrinth/daedalus/{} (support@modrinth.com)",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)) {
|
||||
headers.insert(reqwest::header::USER_AGENT, header);
|
||||
}
|
||||
|
||||
reqwest::Client::builder()
|
||||
.tcp_keepalive(Some(std::time::Duration::from_secs(10)))
|
||||
.timeout(std::time::Duration::from_secs(15))
|
||||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[tracing::instrument(skip(bytes, semaphore))]
|
||||
pub async fn upload_file_to_bucket(
|
||||
@@ -78,7 +76,7 @@ pub async fn upload_file_to_bucket(
|
||||
BUCKET.put_object(key.clone(), &bytes).await
|
||||
}
|
||||
.map_err(|err| ErrorKind::S3 {
|
||||
inner: err,
|
||||
inner: Box::new(err),
|
||||
file: path.clone(),
|
||||
});
|
||||
|
||||
@@ -203,7 +201,7 @@ pub async fn download_file(
|
||||
inner: err,
|
||||
item: url.to_string(),
|
||||
}
|
||||
.into())
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro check && astro build",
|
||||
"lint": "astro check",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
@@ -18,4 +19,4 @@
|
||||
"starlight-openapi": "^0.14.0",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ This project is part of our [monorepo](https://github.com/modrinth/code). You ca
|
||||
|
||||
[labrinth] is the Rust-based backend serving Modrinth's API with the help of the [Actix](https://actix.rs) framework. To get started with a labrinth instance, install docker, docker-compose (which comes with Docker), and [Rust]. The initial startup can be done simply with the command `docker-compose up`, or with `docker compose up` (Compose V2 and later). That will deploy a PostgreSQL database on port 5432 and a MeiliSearch instance on port 7700. To run the API itself, you'll need to use the `cargo run` command, this will deploy the API on port 8000.
|
||||
|
||||
Now, you'll have to install the sqlx CLI, which can be done with cargo:
|
||||
To get a basic configuration, copy the `.env.local` file to `.env`. Now, you'll have to install the sqlx CLI, which can be done with cargo:
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/launchbadge/sqlx sqlx-cli --no-default-features --features postgres,rustls
|
||||
@@ -53,8 +53,12 @@ If you would like 'placeholder_category' to be marked as supporting modpacks too
|
||||
INSERT INTO categories VALUES (0, 'placeholder_category', 2); -- modloader id, supported type id
|
||||
```
|
||||
|
||||
You can find more example SQL statements for seeding the database in the `apps/labrinth/tests/files/dummy_data.sql` file.
|
||||
|
||||
The majority of configuration is done at runtime using [dotenvy](https://crates.io/crates/dotenvy) and the `.env` file. Each of the variables and what they do can be found in the dropdown below. Additionally, there are three command line options that can be used to specify to MeiliSearch what you want to do.
|
||||
|
||||
During development, you might notice that changes made directly to entities in the PostgreSQL database do not seem to take effect. This is often because the Redis cache still holds outdated data. To ensure your updates are reflected, clear the cache by e.g. running `redis-cli FLUSHALL`, which will force Labrinth to fetch the latest data from the database the next time it is needed.
|
||||
|
||||
<details>
|
||||
<summary>.env variables & command line options</summary>
|
||||
|
||||
@@ -73,14 +77,18 @@ The majority of configuration is done at runtime using [dotenvy](https://crates.
|
||||
`MEILISEARCH_KEY`: The name that MeiliSearch is given
|
||||
`BIND_ADDR`: The bind address for the server. Supports both IPv4 and IPv6
|
||||
`MOCK_FILE_PATH`: The path used to store uploaded files; this has no default value and will panic if unspecified
|
||||
`SMTP_USERNAME`: The username used to authenticate with the SMTP server
|
||||
`SMTP_PASSWORD`: The password associated with the `SMTP_USERNAME` for SMTP authentication
|
||||
`SMTP_HOST`: The hostname or IP address of the SMTP server
|
||||
`SMTP_PORT`: The port number on which the SMTP server is listening (commonly 25, 465, or 587)
|
||||
`SMTP_TLS`: The TLS mode to use for the SMTP connection, which can be one of the following: `none`, `opportunistic_start_tls`, `requires_start_tls`, `tls`
|
||||
|
||||
#### 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:
|
||||
`BACKBLAZE_KEY_ID`, `BACKBLAZE_KEY`, `BACKBLAZE_BUCKET_ID`
|
||||
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_BUCKET_NAME`
|
||||
The S3 configuration options are fairly self-explanatory in name, so here's simply their names:
|
||||
`S3_ACCESS_TOKEN`, `S3_SECRET`, `S3_URL`, `S3_REGION`, `S3_PUBLIC_BUCKET_NAME`, `S3_PRIVATE_BUCKET_NAME`, `S3_USES_PATH_STYLE_BUCKETS`
|
||||
|
||||
#### Search, OAuth, and miscellaneous options
|
||||
|
||||
|
||||