Compare commits
108 Commits
chore/post
...
alex/auth-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f07bc86711 | ||
|
|
23f0c1dbf7 | ||
|
|
8abbc021ea | ||
|
|
ed8ff79809 | ||
|
|
f210de563d | ||
|
|
e410a07cac | ||
|
|
9f93cd8705 | ||
|
|
dd391be095 | ||
|
|
f84f8c1c2b | ||
|
|
301967d204 | ||
|
|
c9b98a6154 | ||
|
|
ab8e474339 | ||
|
|
8a26011e76 | ||
|
|
d4de1dc9a1 | ||
|
|
4e3bd4e282 | ||
|
|
d24528f6a6 | ||
|
|
1b1d41605b | ||
|
|
6955731def | ||
|
|
4386891716 | ||
|
|
6741aba880 | ||
|
|
ee8ee7af82 | ||
|
|
a2e323c9ee | ||
|
|
f8fb23e05f | ||
|
|
a3839461cf | ||
|
|
858c7e393f | ||
|
|
0278241006 | ||
|
|
3afb682fc6 | ||
|
|
06f1df1995 | ||
|
|
3489771d2e | ||
|
|
448ae5a2b7 | ||
|
|
72340790e5 | ||
|
|
c9423fe478 | ||
|
|
02a850ae63 | ||
|
|
ede6d0c3cc | ||
|
|
7685989a8c | ||
|
|
4e8ebb5e5c | ||
|
|
3f77ab19ed | ||
|
|
d3d0c8c523 | ||
|
|
4e093131f3 | ||
|
|
6ca8a4e5fd | ||
|
|
63b15ded60 | ||
|
|
85e65aeffe | ||
|
|
ad44398492 | ||
|
|
a4ba41bf15 | ||
|
|
4441be5380 | ||
|
|
c0accb42fa | ||
|
|
7223c2b197 | ||
|
|
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 |
@@ -1,6 +1,6 @@
|
|||||||
# Windows has stack overflows when calling from Tauri, so we increase compiler size
|
# Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler
|
||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220"]
|
rustflags = ["-C", "link-args=/STACK:16777220"]
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/1-app-bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Please confirm the following.
|
label: Please confirm the following.
|
||||||
options:
|
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
|
required: true
|
||||||
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/2-web-bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Please confirm the following.
|
label: Please confirm the following.
|
||||||
options:
|
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
|
required: true
|
||||||
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/3-api-bug.yml
vendored
@@ -6,7 +6,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Please confirm the following.
|
label: Please confirm the following.
|
||||||
options:
|
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
|
required: true
|
||||||
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
- label: I have tried resolving the issue using the [support portal](https://support.modrinth.com)
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/4-feature-request.yml
vendored
@@ -7,7 +7,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Please confirm the following.
|
label: Please confirm the following.
|
||||||
options:
|
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
|
required: true
|
||||||
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
|
- label: I have checked that this feature request is not on our [roadmap](https://roadmap.modrinth.com)
|
||||||
required: true
|
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)
|
- name: Rust setup (mac)
|
||||||
if: startsWith(matrix.platform, 'macos')
|
if: startsWith(matrix.platform, 'macos')
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
|
|
||||||
- name: Rust setup
|
- name: Rust setup
|
||||||
if: "!startsWith(matrix.platform, 'macos')"
|
if: "!startsWith(matrix.platform, 'macos')"
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
rustflags: ''
|
||||||
|
|
||||||
- name: Setup rust cache
|
- name: Setup rust cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -72,10 +72,10 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-rust-target-
|
${{ runner.os }}-rust-target-
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: .nvmrc
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: Install pnpm via corepack
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
98
.github/workflows/turbo-ci.yml
vendored
@@ -2,7 +2,7 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
merge_group:
|
merge_group:
|
||||||
@@ -10,71 +10,69 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build, Test, and Lint
|
name: Lint and Test
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Ensure pnpm output is colored in GitHub Actions logs
|
||||||
|
FORCE_COLOR: 3
|
||||||
|
# Make cargo nextest successfully ignore projects without tests
|
||||||
|
NEXTEST_NO_TESTS: pass
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: 📥 Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
- name: Cache turbo build setup
|
- name: 🧰 Install build dependencies
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: .turbo
|
|
||||||
key: ${{ runner.os }}-turbo-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-turbo-
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||||
|
|
||||||
- name: Setup Node.JS environment
|
- name: 🧰 Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: 🧰 Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version-file: .nvmrc
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install pnpm via corepack
|
- name: 🧰 Setup Rust toolchain
|
||||||
shell: bash
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
run: |
|
|
||||||
corepack enable
|
|
||||||
corepack prepare --activate
|
|
||||||
|
|
||||||
- name: Get pnpm store directory
|
|
||||||
id: pnpm-cache
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
name: Setup pnpm cache
|
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
rustflags: ''
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
components: clippy, rustfmt
|
||||||
restore-keys: |
|
cache: false
|
||||||
${{ runner.os }}-pnpm-store-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: 🧰 Setup nextest
|
||||||
|
uses: taiki-e/install-action@nextest
|
||||||
|
|
||||||
|
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||||
|
# back to a cached cargo install
|
||||||
|
- name: 🧰 Setup cargo-sqlx
|
||||||
|
uses: AlexTMjugador/cache-cargo-install-action@feat/features-support
|
||||||
|
with:
|
||||||
|
tool: sqlx-cli
|
||||||
|
locked: false
|
||||||
|
no-default-features: true
|
||||||
|
features: rustls,postgres
|
||||||
|
|
||||||
|
- name: 💨 Setup Turbo cache
|
||||||
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
|
|
||||||
|
- name: 🧰 Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Build
|
- name: ⚙️ Start services
|
||||||
run: pnpm build
|
run: docker compose up --wait
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
|
|
||||||
- name: Lint
|
- name: ⚙️ Setup Labrinth environment and database
|
||||||
run: pnpm lint
|
working-directory: apps/labrinth
|
||||||
env:
|
run: |
|
||||||
SQLX_OFFLINE: true
|
cp .env.local .env
|
||||||
|
sqlx database setup
|
||||||
|
|
||||||
- name: Start docker compose
|
- name: 🔍 Lint and test
|
||||||
run: docker compose up -d
|
run: pnpm run ci
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: pnpm test
|
|
||||||
env:
|
|
||||||
SQLX_OFFLINE: true
|
|
||||||
DATABASE_URL: postgresql://labrinth:labrinth@localhost/postgres
|
|
||||||
|
|||||||
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="CommitMessageInspectionProfile">
|
||||||
|
<profile version="1.0">
|
||||||
|
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
1
.vscode/settings.json
vendored
@@ -2,6 +2,7 @@
|
|||||||
"prettier.endOfLine": "lf",
|
"prettier.endOfLine": "lf",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||||
|
"editor.detectIndentation": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
}
|
}
|
||||||
|
|||||||
5151
Cargo.lock
generated
234
Cargo.toml
@@ -1,25 +1,227 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = '2'
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
'./packages/app-lib',
|
"apps/app",
|
||||||
'./apps/app-playground',
|
"apps/app-playground",
|
||||||
'./apps/app',
|
"apps/daedalus_client",
|
||||||
'./apps/labrinth',
|
"apps/labrinth",
|
||||||
'./apps/daedalus_client',
|
"packages/app-lib",
|
||||||
'./packages/daedalus',
|
"packages/ariadne",
|
||||||
'./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"
|
||||||
|
heck = "0.5.0"
|
||||||
|
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
|
# Optimize for speed and reduce size on release builds
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort" # Strip expensive panic clean-up logic
|
opt-level = "s" # Optimize for binary size
|
||||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
strip = true # Remove debug symbols
|
||||||
lto = true # Enables link to optimizations
|
lto = true # Enables link to optimizations
|
||||||
opt-level = "s" # Optimize for binary size
|
panic = "abort" # Strip expensive panic clean-up logic
|
||||||
strip = true # Remove debug symbols
|
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "51907c6" }
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@modrinth/app-frontend",
|
"name": "@modrinth/app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.9.4",
|
"version": "0.9.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -9,20 +9,21 @@
|
|||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
|
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
|
"test": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
"@modrinth/ui": "workspace:*",
|
"@modrinth/ui": "workspace:*",
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
"@sentry/vue": "^8.27.0",
|
"@sentry/vue": "^8.27.0",
|
||||||
"@geometrically/minecraft-motd-parser": "^1.1.4",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
"@tauri-apps/api": "^2.1.1",
|
"@tauri-apps/plugin-dialog": "^2.2.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||||
"@tauri-apps/plugin-os": "^2.2.0",
|
"@tauri-apps/plugin-os": "^2.2.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.1",
|
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.3.0",
|
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||||
"@tauri-apps/plugin-window-state": "^2.2.0",
|
|
||||||
"@vintl/vintl": "^4.4.1",
|
"@vintl/vintl": "^4.4.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"floating-vue": "^5.2.2",
|
"floating-vue": "^5.2.2",
|
||||||
@@ -39,11 +40,12 @@
|
|||||||
"@eslint/compat": "^1.1.1",
|
"@eslint/compat": "^1.1.1",
|
||||||
"@formatjs/cli": "^6.2.12",
|
"@formatjs/cli": "^6.2.12",
|
||||||
"@nuxt/eslint-config": "^0.5.6",
|
"@nuxt/eslint-config": "^0.5.6",
|
||||||
|
"@taijased/vue-render-tracker": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-config-custom": "workspace:*",
|
"eslint-config-custom": "workspace:*",
|
||||||
"eslint-plugin-turbo": "^2.1.1",
|
"eslint-plugin-turbo": "^2.5.4",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.74.1",
|
"sass": "^1.74.1",
|
||||||
@@ -51,8 +53,7 @@
|
|||||||
"tsconfig": "workspace:*",
|
"tsconfig": "workspace:*",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^5.4.6",
|
"vite": "^5.4.6",
|
||||||
"vue-tsc": "^2.1.6",
|
"vue-tsc": "^2.1.6"
|
||||||
"@taijased/vue-render-tracker": "^1.0.7"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.4.0",
|
"packageManager": "pnpm@9.4.0",
|
||||||
"web-types": "../../web-types.json"
|
"web-types": "../../web-types.json"
|
||||||
|
|||||||
@@ -19,12 +19,19 @@ import {
|
|||||||
WorldIcon,
|
WorldIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} 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 { useLoading, useTheming } from '@/store/state'
|
||||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.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 Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||||
import SplashScreen from '@/components/ui/SplashScreen.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 { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||||
|
|
||||||
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const news = ref([])
|
const news = ref([])
|
||||||
@@ -590,7 +599,7 @@ function handleAuxClick(e) {
|
|||||||
</h4>
|
</h4>
|
||||||
<p class="my-1 text-sm text-secondary leading-tight">{{ item.summary }}</p>
|
<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">
|
<p class="text-right text-sm text-secondary opacity-60 leading-tight m-0">
|
||||||
{{ dayjs(item.date).fromNow() }}
|
{{ formatRelativeTime(dayjs(item.date).toISOString()) }}
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
<hr
|
<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-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
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 {
|
@font-face {
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 400;
|
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 {
|
@font-face {
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
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 {
|
@font-face {
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
font-weight: 600;
|
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 {
|
.font-minecraft {
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
size="36px"
|
size="36px"
|
||||||
:src="
|
:src="
|
||||||
selectedAccount
|
selectedAccount
|
||||||
? `https://mc-heads.net/avatar/${selectedAccount.id}/128`
|
? `https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`
|
||||||
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
: 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<span>{{ selectedAccount ? selectedAccount.username : 'Select account' }}</span>
|
<span>{{ selectedAccount ? selectedAccount.profile.name : 'Select account' }}</span>
|
||||||
<span class="text-secondary text-xs">Minecraft account</span>
|
<span class="text-secondary text-xs">Minecraft account</span>
|
||||||
</div>
|
</div>
|
||||||
<DropdownIcon class="w-5 h-5 shrink-0" />
|
<DropdownIcon class="w-5 h-5 shrink-0" />
|
||||||
@@ -28,12 +28,17 @@
|
|||||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||||
>
|
>
|
||||||
<div v-if="selectedAccount" class="selected account">
|
<div v-if="selectedAccount" class="selected account">
|
||||||
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.id}/128`" />
|
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`" />
|
||||||
<div>
|
<div>
|
||||||
<h4>{{ selectedAccount.username }}</h4>
|
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||||
<p>Selected</p>
|
<p>Selected</p>
|
||||||
</div>
|
</div>
|
||||||
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
|
<Button
|
||||||
|
v-tooltip="'Log out'"
|
||||||
|
icon-only
|
||||||
|
color="raised"
|
||||||
|
@click="logout(selectedAccount.profile.id)"
|
||||||
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,12 +49,12 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||||
<div v-for="account in displayAccounts" :key="account.id" class="account-row">
|
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||||
<Button class="option account" @click="setAccount(account)">
|
<Button class="option account" @click="setAccount(account)">
|
||||||
<Avatar :src="`https://mc-heads.net/avatar/${account.id}/128`" class="icon" />
|
<Avatar :src="`https://mc-heads.net/avatar/${account.profile.id}/128`" class="icon" />
|
||||||
<p>{{ account.username }}</p>
|
<p>{{ account.profile.name }}</p>
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
|
<Button v-tooltip="'Log out'" icon-only @click="logout(account.profile.id)">
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -101,16 +106,16 @@ defineExpose({
|
|||||||
await refreshValues()
|
await refreshValues()
|
||||||
|
|
||||||
const displayAccounts = computed(() =>
|
const displayAccounts = computed(() =>
|
||||||
accounts.value.filter((account) => defaultUser.value !== account.id),
|
accounts.value.filter((account) => defaultUser.value !== account.profile.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedAccount = computed(() =>
|
const selectedAccount = computed(() =>
|
||||||
accounts.value.find((account) => account.id === defaultUser.value),
|
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function setAccount(account) {
|
async function setAccount(account) {
|
||||||
defaultUser.value = account.id
|
defaultUser.value = account.profile.id
|
||||||
await set_default_user(account.id).catch(handleError)
|
await set_default_user(account.profile.id).catch(handleError)
|
||||||
emit('change')
|
emit('change')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { handleSevereError } from '@/store/error.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 { install } from '@/helpers/profile.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
@@ -92,7 +92,7 @@ async function loginMinecraft() {
|
|||||||
const loggedIn = await login_flow()
|
const loggedIn = await login_flow()
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await set_default_user(loggedIn.id).catch(handleError)
|
await set_default_user(loggedIn.profile.id).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
await trackEvent('AccountLogIn', { source: 'ErrorModal' })
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { finish_install, kill, run } from '@/helpers/profile'
|
import { finish_install, kill, run } from '@/helpers/profile'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
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 { handleSevereError } from '@/store/error.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
@@ -173,7 +172,9 @@ onUnmounted(() => unlisten())
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
|
||||||
<TimerIcon />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalWrapper ref="modal" header="Create instance">
|
<ModalWrapper ref="modal" header="Creating an instance">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
|
<Chips v-model="creationType" :items="['custom', 'from file', 'import from launcher']" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -124,8 +124,11 @@ import dayjs from 'dayjs'
|
|||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
import { install as installVersion } from '@/store/install.js'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -168,6 +171,9 @@ async function install() {
|
|||||||
installing.value = false
|
installing.value = false
|
||||||
emit('install', props.project.project_id ?? props.project.id)
|
emit('install', props.project.project_id ?? props.project.id)
|
||||||
},
|
},
|
||||||
|
(profile) => {
|
||||||
|
router.push(`/instance/${profile}`)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Avatar, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
@@ -18,6 +18,8 @@ import type { Dayjs } from 'dayjs'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
credentials: unknown | null
|
credentials: unknown | null
|
||||||
signIn: () => void
|
signIn: () => void
|
||||||
@@ -205,7 +207,9 @@ onUnmounted(() => {
|
|||||||
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
You sent <span class="font-bold">{{ friend.username }}</span> a friend request
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</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>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<template v-if="friend.id === userCredentials.user_id">
|
<template v-if="friend.id === userCredentials.user_id">
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ const confirmModal = ref(null)
|
|||||||
const installing = ref(false)
|
const installing = ref(false)
|
||||||
|
|
||||||
const onInstall = ref(() => {})
|
const onInstall = ref(() => {})
|
||||||
|
const onCreateInstance = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (projectVal, versionIdVal, callback) => {
|
show: (projectVal, versionIdVal, callback, createInstanceCallback) => {
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
versionId.value = versionIdVal
|
versionId.value = versionIdVal
|
||||||
installing.value = false
|
installing.value = false
|
||||||
confirmModal.value.show()
|
confirmModal.value.show()
|
||||||
|
|
||||||
onInstall.value = callback
|
onInstall.value = callback
|
||||||
|
onCreateInstance.value = createInstanceCallback
|
||||||
|
|
||||||
trackEvent('PackInstallStart')
|
trackEvent('PackInstallStart')
|
||||||
},
|
},
|
||||||
@@ -36,6 +38,7 @@ async function install() {
|
|||||||
versionId.value,
|
versionId.value,
|
||||||
project.value.title,
|
project.value.title,
|
||||||
project.value.icon_url,
|
project.value.icon_url,
|
||||||
|
onCreateInstance.value,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
trackEvent('PackInstall', {
|
trackEvent('PackInstall', {
|
||||||
id: project.value.id,
|
id: project.value.id,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Checkbox } from '@modrinth/ui'
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { get } from '@/helpers/settings'
|
import { get } from '@/helpers/settings.ts'
|
||||||
import { edit } from '@/helpers/profile'
|
import { edit } from '@/helpers/profile'
|
||||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
||||||
|
|
||||||
@@ -25,9 +25,8 @@ const editProfileObject = computed(() => {
|
|||||||
hooks?: Hooks
|
hooks?: Hooks
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (overrideHooks.value) {
|
// When hooks are not overridden per-instance, we want to clear them
|
||||||
editProfile.hooks = hooks.value
|
editProfile.hooks = overrideHooks.value ? hooks.value : {}
|
||||||
}
|
|
||||||
|
|
||||||
return editProfile
|
return editProfile
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { handleError } from '@/store/notifications'
|
|||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
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'
|
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Checkbox, Toggle } from '@modrinth/ui'
|
|||||||
import { computed, ref, type Ref, watch } from 'vue'
|
import { computed, ref, type Ref, watch } from 'vue'
|
||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { get } from '@/helpers/settings'
|
import { get } from '@/helpers/settings.ts'
|
||||||
import { edit } from '@/helpers/profile'
|
import { edit } from '@/helpers/profile'
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
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 { useTheming } from '@/store/state'
|
||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.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()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ConfirmModal } from '@modrinth/ui'
|
import { ConfirmModal } from '@modrinth/ui'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
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()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { NewModal as Modal } from '@modrinth/ui'
|
import { NewModal as Modal } from '@modrinth/ui'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
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()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ShareModal } from '@modrinth/ui'
|
import { ShareModal } from '@modrinth/ui'
|
||||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
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()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
import { TeleportDropdownMenu, ThemeSelector, Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
import { useTheming } from '@/store/state'
|
||||||
import { get, set } from '@/helpers/settings'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { getOS } from '@/helpers/utils'
|
import { getOS } from '@/helpers/utils'
|
||||||
|
import type { ColorTheme } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -24,13 +25,13 @@ watch(
|
|||||||
|
|
||||||
<ThemeSelector
|
<ThemeSelector
|
||||||
:update-color-theme="
|
:update-color-theme="
|
||||||
(theme) => {
|
(theme: ColorTheme) => {
|
||||||
themeStore.setThemeState(theme)
|
themeStore.setThemeState(theme)
|
||||||
settings.theme = theme
|
settings.theme = theme
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
:current-theme="settings.theme"
|
:current-theme="settings.theme"
|
||||||
:theme-options="themeStore.themeOptions"
|
:theme-options="themeStore.getThemeOptions()"
|
||||||
system-theme-color="system"
|
system-theme-color="system"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -80,10 +81,28 @@ watch(
|
|||||||
id="opening-page"
|
id="opening-page"
|
||||||
v-model="settings.default_page"
|
v-model="settings.default_page"
|
||||||
name="Opening page dropdown"
|
name="Opening page dropdown"
|
||||||
|
class="w-40"
|
||||||
:options="['Home', 'Library']"
|
:options="['Home', 'Library']"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 class="mt-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Toggle sidebar</h2>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { get, set } from '@/helpers/settings'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
import { get_max_memory } from '@/helpers/jre'
|
||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
|
|||||||
@@ -2,18 +2,15 @@
|
|||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
import { useTheming } from '@/store/state'
|
||||||
import { ref, watch } from 'vue'
|
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 themeStore = useTheming()
|
||||||
|
|
||||||
const settings = ref(await get())
|
const settings = ref(await getSettings())
|
||||||
const options = ref(['project_background', 'page_path', 'worlds_tab'])
|
const options = ref<FeatureFlag[]>(Object.keys(DEFAULT_FEATURE_FLAGS))
|
||||||
|
|
||||||
function getStoreValue(key: string) {
|
function setFeatureFlag(key: string, value: boolean) {
|
||||||
return themeStore.featureFlags[key] ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStoreValue(key: string, value: boolean) {
|
|
||||||
themeStore.featureFlags[key] = value
|
themeStore.featureFlags[key] = value
|
||||||
settings.value.feature_flags[key] = value
|
settings.value.feature_flags[key] = value
|
||||||
}
|
}
|
||||||
@@ -21,7 +18,7 @@ function setStoreValue(key: string, value: boolean) {
|
|||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
async () => {
|
async () => {
|
||||||
await set(settings.value)
|
await setSettings(settings.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
@@ -36,8 +33,8 @@ watch(
|
|||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
id="advanced-rendering"
|
id="advanced-rendering"
|
||||||
:model-value="getStoreValue(option)"
|
:model-value="themeStore.getFeatureFlag(option)"
|
||||||
@update:model-value="() => setStoreValue(option, !themeStore.featureFlags[option])"
|
@update:model-value="() => setFeatureFlag(option, !themeStore.getFeatureFlag(option))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { get, set } from '@/helpers/settings'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Slider } from '@modrinth/ui'
|
import { Button, Slider } from '@modrinth/ui'
|
||||||
import { ref, watch } from 'vue'
|
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 { purge_cache_types } from '@/helpers/cache.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ import {
|
|||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
} from '@modrinth/assets'
|
} 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 { useVIntl } from '@vintl/vintl'
|
||||||
import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
|
import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { showProfileInFolder } from '@/helpers/utils'
|
import { showProfileInFolder } from '@/helpers/utils'
|
||||||
@@ -25,6 +32,7 @@ import { handleError } from '@/store/notifications'
|
|||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -144,7 +152,7 @@ onUnmounted(() => {
|
|||||||
<template v-if="instance.last_played">
|
<template v-if="instance.last_played">
|
||||||
{{
|
{{
|
||||||
formatMessage(commonMessages.playedLabel, {
|
formatMessage(commonMessages.playedLabel, {
|
||||||
time: dayjs(instance.last_played).fromNow(),
|
time: formatRelativeTime(instance.last_played.toISOString()),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
@@ -153,7 +161,7 @@ onUnmounted(() => {
|
|||||||
•
|
•
|
||||||
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
<span v-if="modpack" class="flex items-center gap-1 truncate text-secondary">
|
||||||
<router-link
|
<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}`"
|
:to="`/project/${modpack.id}`"
|
||||||
>
|
>
|
||||||
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
<Avatar :src="modpack.icon_url" size="16px" class="shrink-0" />
|
||||||
|
|||||||
@@ -13,16 +13,17 @@ import {
|
|||||||
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
||||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||||
import InstanceItem from '@/components/ui/world/InstanceItem.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 type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useTheming } from '@/store/theme'
|
import { useTheming } from '@/store/theme.ts'
|
||||||
import { kill } from '@/helpers/profile'
|
import { kill, run } from '@/helpers/profile'
|
||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { process_listener, profile_listener } from '@/helpers/events'
|
import { process_listener, profile_listener } from '@/helpers/events'
|
||||||
import { get_all } from '@/helpers/process'
|
import { get_all } from '@/helpers/process'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { handleSevereError } from '@/store/error'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
recentInstances: GameInstance[]
|
recentInstances: GameInstance[]
|
||||||
@@ -54,7 +55,9 @@ type WorldJumpBackInItem = BaseJumpBackInItem & {
|
|||||||
|
|
||||||
type JumpBackInItem = InstanceJumpBackInItem | WorldJumpBackInItem
|
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(() => {
|
await populateJumpBackIn().catch(() => {
|
||||||
console.error('Failed to populate jump back in')
|
console.error('Failed to populate jump back in')
|
||||||
})
|
})
|
||||||
@@ -66,66 +69,71 @@ await populateJumpBackIn().catch(() => {
|
|||||||
|
|
||||||
async function populateJumpBackIn() {
|
async function populateJumpBackIn() {
|
||||||
console.info('Repopulating jump back in...')
|
console.info('Repopulating jump back in...')
|
||||||
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN)
|
|
||||||
|
|
||||||
const worldItems: WorldJumpBackInItem[] = []
|
const worldItems: WorldJumpBackInItem[] = []
|
||||||
worlds.forEach((world) => {
|
|
||||||
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
|
||||||
|
|
||||||
if (!instance || !world.last_played) {
|
if (showWorlds.value) {
|
||||||
return
|
const worlds = await get_recent_worlds(MAX_JUMP_BACK_IN, ['normal', 'favorite'])
|
||||||
}
|
|
||||||
|
|
||||||
worldItems.push({
|
worlds.forEach((world) => {
|
||||||
type: 'world',
|
const instance = props.recentInstances.find((instance) => instance.path === world.profile)
|
||||||
last_played: dayjs(world.last_played),
|
|
||||||
world: world,
|
|
||||||
instance: instance,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const servers: {
|
if (!instance || !world.last_played) {
|
||||||
instancePath: string
|
return
|
||||||
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
|
worldItems.push({
|
||||||
await Promise.all(
|
type: 'world',
|
||||||
servers.map(({ instancePath, address }) =>
|
last_played: dayjs(world.last_played),
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
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[] = []
|
const instanceItems: InstanceJumpBackInItem[] = []
|
||||||
props.recentInstances.forEach((instance) => {
|
for (const instance of props.recentInstances) {
|
||||||
if (worldItems.some((item) => item.instance.path === instance.path) || !instance.last_played) {
|
const worldItem = worldItems.find((item) => item.instance.path === instance.path)
|
||||||
return
|
if ((worldItem && worldItem.last_played.isAfter(TWO_WEEKS_AGO)) || !instance.last_played) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceItems.push({
|
instanceItems.push({
|
||||||
@@ -133,13 +141,13 @@ async function populateJumpBackIn() {
|
|||||||
last_played: dayjs(instance.last_played),
|
last_played: dayjs(instance.last_played),
|
||||||
instance: instance,
|
instance: instance,
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
|
||||||
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
|
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
|
||||||
jumpBackInItems.value = items.filter(
|
jumpBackInItems.value = items
|
||||||
(item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO),
|
.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) {
|
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) {
|
async function stopInstance(path: string) {
|
||||||
await kill(path).catch(handleError)
|
await kill(path).catch(handleError)
|
||||||
trackEvent('InstanceStop', {
|
trackEvent('InstanceStop', {
|
||||||
@@ -209,11 +229,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
<div v-if="jumpBackInItems.length > 0" class="flex flex-col gap-2">
|
||||||
<HeadingLink
|
<HeadingLink v-if="theme.getFeatureFlag('worlds_tab')" to="/worlds" class="mt-1">
|
||||||
v-if="(theme.featureFlags as Record<string, boolean>)['worlds_tab']"
|
|
||||||
to="/worlds"
|
|
||||||
class="mt-1"
|
|
||||||
>
|
|
||||||
Jump back in
|
Jump back in
|
||||||
</HeadingLink>
|
</HeadingLink>
|
||||||
<span
|
<span
|
||||||
@@ -222,7 +238,7 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
Jump back in
|
Jump back in
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col w-full gap-2">
|
<div class="grid-when-huge flex flex-col w-full gap-2">
|
||||||
<template
|
<template
|
||||||
v-for="item in jumpBackInItems"
|
v-for="item in jumpBackInItems"
|
||||||
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
:key="`${item.instance.path}-${item.type === 'world' ? getWorldIdentifier(item.world) : 'instance'}`"
|
||||||
@@ -246,7 +262,7 @@ onUnmounted(() => {
|
|||||||
:rendered-motd="
|
:rendered-motd="
|
||||||
item.world.type === 'server' ? serverData[item.world.address].renderedMotd : undefined
|
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="
|
:game-mode="
|
||||||
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
item.world.type === 'singleplayer' ? GAME_MODES[item.world.game_mode] : undefined
|
||||||
"
|
"
|
||||||
@@ -259,6 +275,7 @@ onUnmounted(() => {
|
|||||||
? refreshServer(item.world.address, item.instance.path)
|
? refreshServer(item.world.address, item.instance.path)
|
||||||
: {}
|
: {}
|
||||||
"
|
"
|
||||||
|
@update="() => populateJumpBackIn()"
|
||||||
@play="
|
@play="
|
||||||
() => {
|
() => {
|
||||||
currentProfile = item.instance.path
|
currentProfile = item.instance.path
|
||||||
@@ -266,6 +283,12 @@ onUnmounted(() => {
|
|||||||
joinWorld(item.world)
|
joinWorld(item.world)
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
@play-instance="
|
||||||
|
() => {
|
||||||
|
currentProfile = item.instance.path
|
||||||
|
playInstance(item.instance)
|
||||||
|
}
|
||||||
|
"
|
||||||
@stop="() => stopInstance(item.instance.path)"
|
@stop="() => stopInstance(item.instance.path)"
|
||||||
/>
|
/>
|
||||||
<InstanceItem v-else :instance="item.instance" />
|
<InstanceItem v-else :instance="item.instance" />
|
||||||
@@ -273,3 +296,9 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type { ServerStatus, ServerWorld, World } from '@/helpers/worlds.ts'
|
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
||||||
import { getWorldIdentifier, showWorldInFolder } from '@/helpers/worlds.ts'
|
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||||
import { formatNumber } from '@modrinth/utils'
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
|
import {
|
||||||
|
useRelativeTime,
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
OverflowMenu,
|
||||||
|
SmartClickable,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
@@ -19,8 +27,8 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, commonMessages, OverflowMenu, SmartClickable } from '@modrinth/ui'
|
|
||||||
import type { MessageDescriptor } from '@vintl/vintl'
|
import type { MessageDescriptor } from '@vintl/vintl'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
@@ -31,11 +39,13 @@ import { useRouter } from 'vue-router'
|
|||||||
import { Tooltip } from 'floating-vue'
|
import { Tooltip } from 'floating-vue'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
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(
|
const props = withDefaults(
|
||||||
@@ -69,6 +79,7 @@ const props = withDefaults(
|
|||||||
playingWorld: false,
|
playingWorld: false,
|
||||||
startingInstance: false,
|
startingInstance: false,
|
||||||
supportsQuickPlay: false,
|
supportsQuickPlay: false,
|
||||||
|
currentProtocol: null,
|
||||||
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
serverStatus: undefined,
|
serverStatus: undefined,
|
||||||
@@ -94,20 +105,6 @@ const serverIncompatible = computed(
|
|||||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||||
)
|
)
|
||||||
|
|
||||||
function getPingLevel(ping: number) {
|
|
||||||
if (ping < 150) {
|
|
||||||
return 5
|
|
||||||
} else if (ping < 300) {
|
|
||||||
return 4
|
|
||||||
} else if (ping < 600) {
|
|
||||||
return 3
|
|
||||||
} else if (ping < 1000) {
|
|
||||||
return 2
|
|
||||||
} else {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -143,10 +140,18 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.play_anyway',
|
id: 'instance.worlds.play_anyway',
|
||||||
defaultMessage: 'Play anyway',
|
defaultMessage: 'Play anyway',
|
||||||
},
|
},
|
||||||
|
playInstance: {
|
||||||
|
id: 'instance.worlds.play_instance',
|
||||||
|
defaultMessage: 'Play instance',
|
||||||
|
},
|
||||||
worldInUse: {
|
worldInUse: {
|
||||||
id: 'instance.worlds.world_in_use',
|
id: 'instance.worlds.world_in_use',
|
||||||
defaultMessage: 'World is in use',
|
defaultMessage: 'World is in use',
|
||||||
},
|
},
|
||||||
|
dontShowOnHome: {
|
||||||
|
id: 'instance.worlds.dont_show_on_home',
|
||||||
|
defaultMessage: `Don't show on Home`,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@@ -241,7 +246,7 @@ const messages = defineMessages({
|
|||||||
<template v-if="world.last_played">
|
<template v-if="world.last_played">
|
||||||
{{
|
{{
|
||||||
formatMessage(commonMessages.playedLabel, {
|
formatMessage(commonMessages.playedLabel, {
|
||||||
time: dayjs(world.last_played).fromNow(),
|
time: formatRelativeTime(dayjs(world.last_played).toISOString()),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</template>
|
</template>
|
||||||
@@ -336,6 +341,12 @@ const messages = defineMessages({
|
|||||||
<ButtonStyled circular type="transparent">
|
<ButtonStyled circular type="transparent">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'play-instance',
|
||||||
|
shown: !!instancePath,
|
||||||
|
disabled: playingInstance,
|
||||||
|
action: () => emit('play-instance'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'play-anyway',
|
id: 'play-anyway',
|
||||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
||||||
@@ -344,7 +355,7 @@ const messages = defineMessages({
|
|||||||
{
|
{
|
||||||
id: 'open-instance',
|
id: 'open-instance',
|
||||||
shown: !!instancePath,
|
shown: !!instancePath,
|
||||||
action: () => router.push(encodeURI(`/instance/${instancePath}/worlds`)),
|
action: () => router.push(encodeURI(`/instance/${instancePath}`)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'refresh',
|
id: 'refresh',
|
||||||
@@ -366,8 +377,25 @@ const messages = defineMessages({
|
|||||||
{
|
{
|
||||||
id: 'open-folder',
|
id: 'open-folder',
|
||||||
shown: world.type === 'singleplayer',
|
shown: world.type === 'singleplayer',
|
||||||
action: () =>
|
action: () => (world.type === 'singleplayer' ? emit('open-folder', world) : {}),
|
||||||
world.type === 'singleplayer' ? showWorldInFolder(instancePath, world.path) : {},
|
},
|
||||||
|
{
|
||||||
|
divider: true,
|
||||||
|
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,
|
divider: true,
|
||||||
@@ -385,6 +413,10 @@ const messages = defineMessages({
|
|||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
|
<template #play-instance>
|
||||||
|
<PlayIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.playInstance) }}
|
||||||
|
</template>
|
||||||
<template #play-anyway>
|
<template #play-anyway>
|
||||||
<PlayIcon aria-hidden="true" />
|
<PlayIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.playAnyway) }}
|
{{ formatMessage(messages.playAnyway) }}
|
||||||
@@ -406,6 +438,10 @@ const messages = defineMessages({
|
|||||||
<template #refresh>
|
<template #refresh>
|
||||||
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
<UpdatedIcon aria-hidden="true" /> {{ formatMessage(commonMessages.refreshButton) }}
|
||||||
</template>
|
</template>
|
||||||
|
<template #dont-show-on-home>
|
||||||
|
<XIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.dontShowOnHome) }}
|
||||||
|
</template>
|
||||||
<template #delete>
|
<template #delete>
|
||||||
<TrashIcon aria-hidden="true" />
|
<TrashIcon aria-hidden="true" />
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
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 { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
@@ -21,10 +28,14 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
|
|
||||||
const name = ref()
|
const name = ref<string>('')
|
||||||
const address = ref()
|
const address = ref<string>('')
|
||||||
const resourcePack = ref('enabled')
|
const resourcePack = ref<ServerPackStatus>('enabled')
|
||||||
const index = ref()
|
const index = ref<number>(0)
|
||||||
|
const displayStatus = ref<DisplayStatus>('normal')
|
||||||
|
const hideFromHome = ref(false)
|
||||||
|
|
||||||
|
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||||
|
|
||||||
async function saveServer() {
|
async function saveServer() {
|
||||||
const serverName = name.value ? name.value : address.value
|
const serverName = name.value ? name.value : address.value
|
||||||
@@ -36,12 +47,23 @@ async function saveServer() {
|
|||||||
address.value,
|
address.value,
|
||||||
resourcePackStatus,
|
resourcePackStatus,
|
||||||
).catch(handleError)
|
).catch(handleError)
|
||||||
|
|
||||||
|
if (newDisplayStatus.value !== displayStatus.value) {
|
||||||
|
await set_world_display_status(
|
||||||
|
props.instance.path,
|
||||||
|
'server',
|
||||||
|
address.value,
|
||||||
|
newDisplayStatus.value,
|
||||||
|
).catch(handleError)
|
||||||
|
}
|
||||||
|
|
||||||
emit('submit', {
|
emit('submit', {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
type: 'server',
|
type: 'server',
|
||||||
index: index.value,
|
index: index.value,
|
||||||
address: address.value,
|
address: address.value,
|
||||||
pack_status: resourcePackStatus,
|
pack_status: resourcePackStatus,
|
||||||
|
display_status: newDisplayStatus.value,
|
||||||
})
|
})
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
@@ -51,6 +73,8 @@ function show(server: ServerWorld) {
|
|||||||
address.value = server.address
|
address.value = server.address
|
||||||
resourcePack.value = server.pack_status
|
resourcePack.value = server.pack_status
|
||||||
index.value = server.index
|
index.value = server.index
|
||||||
|
displayStatus.value = server.display_status
|
||||||
|
hideFromHome.value = server.display_status === 'hidden'
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +99,7 @@ const titleMessage = defineMessage({
|
|||||||
v-model:address="address"
|
v-model:address="address"
|
||||||
v-model:resource-pack="resourcePack"
|
v-model:resource-pack="resourcePack"
|
||||||
/>
|
/>
|
||||||
|
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled color="brand">
|
||||||
<button :disabled="!address" @click="saveServer">
|
<button :disabled="!address" @click="saveServer">
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
|
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 ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import type { SingleplayerWorld } from '@/helpers/worlds.ts'
|
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||||
import { rename_world, reset_world_icon } 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 { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: [path: string, name: string, removeIcon: boolean]
|
submit: [path: string, name: string, removeIcon: boolean, displayStatus: DisplayStatus]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -25,6 +26,10 @@ const icon = ref()
|
|||||||
const name = ref()
|
const name = ref()
|
||||||
const path = ref()
|
const path = ref()
|
||||||
const removeIcon = ref(false)
|
const removeIcon = ref(false)
|
||||||
|
const displayStatus = ref<DisplayStatus>('normal')
|
||||||
|
const hideFromHome = ref(false)
|
||||||
|
|
||||||
|
const newDisplayStatus = computed(() => (hideFromHome.value ? 'hidden' : 'normal'))
|
||||||
|
|
||||||
async function saveWorld() {
|
async function saveWorld() {
|
||||||
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
await rename_world(props.instance.path, path.value, name.value).catch(handleError)
|
||||||
@@ -32,8 +37,16 @@ async function saveWorld() {
|
|||||||
if (removeIcon.value) {
|
if (removeIcon.value) {
|
||||||
await reset_world_icon(props.instance.path, path.value).catch(handleError)
|
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()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +54,9 @@ function show(world: SingleplayerWorld) {
|
|||||||
name.value = world.name
|
name.value = world.name
|
||||||
path.value = world.path
|
path.value = world.path
|
||||||
icon.value = world.icon
|
icon.value = world.icon
|
||||||
|
displayStatus.value = world.display_status
|
||||||
|
hideFromHome.value = world.display_status === 'hidden'
|
||||||
|
removeIcon.value = false
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +103,7 @@ const messages = defineMessages({
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
<HideFromHomeOption v-model="hideFromHome" class="mt-3" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2 mt-4">
|
||||||
<ButtonStyled color="brand">
|
<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'
|
import { create } from './profile'
|
||||||
|
|
||||||
// Installs pack from a version ID
|
// 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 = {
|
const location = {
|
||||||
type: 'fromVersionId',
|
type: 'fromVersionId',
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
@@ -24,6 +30,7 @@ export async function create_profile_and_install(projectId, versionId, packTitle
|
|||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
createInstanceCallback(profile)
|
||||||
|
|
||||||
return await invoke('plugin:pack|pack_install', { location, 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 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 = {
|
type MemorySettings = {
|
||||||
maximum: number
|
maximum: number
|
||||||
}
|
}
|
||||||
@@ -88,6 +114,7 @@ type AppSettings = {
|
|||||||
collapsed_navigation: boolean
|
collapsed_navigation: boolean
|
||||||
advanced_rendering: boolean
|
advanced_rendering: boolean
|
||||||
native_decorations: boolean
|
native_decorations: boolean
|
||||||
|
worlds_in_home: boolean
|
||||||
|
|
||||||
telemetry: boolean
|
telemetry: boolean
|
||||||
discord_rpc: boolean
|
discord_rpc: boolean
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ type BaseWorld = {
|
|||||||
name: string
|
name: string
|
||||||
last_played?: string
|
last_played?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
|
display_status: DisplayStatus
|
||||||
|
type: WorldType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WorldType = 'singleplayer' | 'server'
|
||||||
|
export type DisplayStatus = 'normal' | 'hidden' | 'favorite'
|
||||||
|
|
||||||
export type SingleplayerWorld = BaseWorld & {
|
export type SingleplayerWorld = BaseWorld & {
|
||||||
type: 'singleplayer'
|
type: 'singleplayer'
|
||||||
path: string
|
path: string
|
||||||
@@ -70,8 +75,11 @@ export type ServerData = {
|
|||||||
renderedMotd?: string
|
renderedMotd?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_recent_worlds(limit: number): Promise<WorldWithProfile[]> {
|
export async function get_recent_worlds(
|
||||||
return await invoke('plugin:worlds|get_recent_worlds', { limit })
|
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[]> {
|
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 })
|
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(
|
export async function rename_world(
|
||||||
instance: string,
|
instance: string,
|
||||||
world: string,
|
world: string,
|
||||||
@@ -230,12 +252,14 @@ export async function refreshServers(
|
|||||||
|
|
||||||
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
export async function refreshWorld(worlds: World[], instancePath: string, worldPath: string) {
|
||||||
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
const index = worlds.findIndex((w) => w.type === 'singleplayer' && w.path === worldPath)
|
||||||
|
const newWorld = await get_singleplayer_world(instancePath, worldPath)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
worlds[index] = await get_singleplayer_world(instancePath, worldPath)
|
worlds[index] = newWorld
|
||||||
sortWorlds(worlds)
|
|
||||||
} else {
|
} 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(
|
export async function handleDefaultProfileUpdateEvent(
|
||||||
|
|||||||
@@ -41,6 +41,9 @@
|
|||||||
"instance.edit-server.title": {
|
"instance.edit-server.title": {
|
||||||
"message": "Edit server"
|
"message": "Edit server"
|
||||||
},
|
},
|
||||||
|
"instance.edit-world.hide-from-home": {
|
||||||
|
"message": "Hide from the Home page"
|
||||||
|
},
|
||||||
"instance.edit-world.name": {
|
"instance.edit-world.name": {
|
||||||
"message": "Name"
|
"message": "Name"
|
||||||
},
|
},
|
||||||
@@ -362,6 +365,9 @@
|
|||||||
"instance.worlds.copy_address": {
|
"instance.worlds.copy_address": {
|
||||||
"message": "Copy address"
|
"message": "Copy address"
|
||||||
},
|
},
|
||||||
|
"instance.worlds.dont_show_on_home": {
|
||||||
|
"message": "Don't show on Home"
|
||||||
|
},
|
||||||
"instance.worlds.filter.available": {
|
"instance.worlds.filter.available": {
|
||||||
"message": "Available"
|
"message": "Available"
|
||||||
},
|
},
|
||||||
@@ -377,6 +383,9 @@
|
|||||||
"instance.worlds.play_anyway": {
|
"instance.worlds.play_anyway": {
|
||||||
"message": "Play anyway"
|
"message": "Play anyway"
|
||||||
},
|
},
|
||||||
|
"instance.worlds.play_instance": {
|
||||||
|
"message": "Play instance"
|
||||||
|
},
|
||||||
"instance.worlds.type.server": {
|
"instance.worlds.type.server": {
|
||||||
"message": "Server"
|
"message": "Server"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,11 @@
|
|||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-if="instance.install_stage.includes('installing')"
|
v-if="
|
||||||
|
['installing', 'pack_installing', 'minecraft_installing'].includes(
|
||||||
|
instance.install_stage,
|
||||||
|
)
|
||||||
|
"
|
||||||
color="brand"
|
color="brand"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
<FilterIcon class="text-secondary h-5 w-5 mr-1" />
|
||||||
<button
|
<button
|
||||||
v-for="filter in filterOptions"
|
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'}`"
|
: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)"
|
@click="toggleArray(selectedFilters, filter.id)"
|
||||||
>
|
>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
path: x.path,
|
path: x.path,
|
||||||
disabled: x.disabled,
|
disabled: x.disabled,
|
||||||
filename: x.file_name,
|
filename: x.file_name,
|
||||||
icon: x.icon,
|
icon: x.icon ?? undefined,
|
||||||
title: x.name,
|
title: x.name,
|
||||||
data: x,
|
data: x,
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
color-fill="text"
|
color-fill="text"
|
||||||
hover-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
|
<DownloadIcon /> Update pack
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-tooltip="`Update`"
|
v-tooltip="`Update`"
|
||||||
:disabled="(item.data as any).updating"
|
:disabled="(item.data as ProjectListEntry).updating"
|
||||||
@click="updateProject(item.data)"
|
@click="updateProject(item.data)"
|
||||||
>
|
>
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
@@ -276,6 +276,7 @@ import {
|
|||||||
RadialHeader,
|
RadialHeader,
|
||||||
Toggle,
|
Toggle,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
import { formatProjectType } from '@modrinth/utils'
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef } from 'vue'
|
||||||
import { computed, onUnmounted, ref, watch } 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 ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
import dayjs from 'dayjs'
|
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({
|
const props = defineProps<{
|
||||||
instance: {
|
instance: GameInstance
|
||||||
type: Object,
|
options: InstanceType<typeof ContextMenu>
|
||||||
default() {
|
offline: boolean
|
||||||
return {}
|
playing: boolean
|
||||||
},
|
versions: Version[]
|
||||||
},
|
installed: boolean
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type ProjectListEntryAuthor = {
|
type ProjectListEntryAuthor = {
|
||||||
name: string
|
name: string
|
||||||
@@ -356,13 +332,15 @@ type ProjectListEntry = {
|
|||||||
author: ProjectListEntryAuthor | null
|
author: ProjectListEntryAuthor | null
|
||||||
version: string | null
|
version: string | null
|
||||||
file_name: string
|
file_name: string
|
||||||
icon: string | null
|
icon: string | undefined
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
updateVersion?: string
|
updateVersion?: string
|
||||||
outdated: boolean
|
outdated: boolean
|
||||||
updated: dayjs.Dayjs
|
updated: dayjs.Dayjs
|
||||||
project_type: string
|
project_type: string
|
||||||
id?: string
|
id?: string
|
||||||
|
updating?: boolean
|
||||||
|
selected?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPackLocked = computed(() => {
|
const isPackLocked = computed(() => {
|
||||||
@@ -375,17 +353,20 @@ const canUpdatePack = computed(() => {
|
|||||||
const exportModal = ref(null)
|
const exportModal = ref(null)
|
||||||
|
|
||||||
const projects = ref<ProjectListEntry[]>([])
|
const projects = ref<ProjectListEntry[]>([])
|
||||||
const selectedFiles = ref([])
|
const selectedFiles = ref<string[]>([])
|
||||||
const selectedProjects = computed(() =>
|
const selectedProjects = computed(() =>
|
||||||
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
|
projects.value.filter((x) => selectedFiles.value.includes(x.file_name)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectionMap = ref(new Map())
|
const selectionMap = ref(new Map())
|
||||||
|
|
||||||
const initProjects = async (cacheBehaviour?) => {
|
const initProjects = async (cacheBehaviour?: CacheBehaviour) => {
|
||||||
const newProjects: ProjectListEntry[] = []
|
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 fetchProjects = []
|
||||||
const fetchVersions = []
|
const fetchVersions = []
|
||||||
|
|
||||||
@@ -397,21 +378,21 @@ const initProjects = async (cacheBehaviour?) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [modrinthProjects, modrinthVersions] = await Promise.all([
|
const [modrinthProjects, modrinthVersions] = await Promise.all([
|
||||||
await get_project_many(fetchProjects).catch(handleError),
|
(await get_project_many(fetchProjects).catch(handleError)) as Project[],
|
||||||
await get_version_many(fetchVersions).catch(handleError),
|
(await get_version_many(fetchVersions).catch(handleError)) as Version[],
|
||||||
])
|
])
|
||||||
|
|
||||||
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
|
const [modrinthTeams, modrinthOrganizations] = await Promise.all([
|
||||||
await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError),
|
(await get_team_many(modrinthProjects.map((x) => x.team)).catch(handleError)) as TeamMember[][],
|
||||||
await get_organization_many(
|
(await get_organization_many(
|
||||||
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
|
modrinthProjects.map((x) => x.organization).filter((x) => !!x),
|
||||||
).catch(handleError),
|
).catch(handleError)) as Organization[],
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const [path, file] of Object.entries(profileProjects)) {
|
for (const [path, file] of Object.entries(profileProjects)) {
|
||||||
if (file.metadata) {
|
if (file.metadata) {
|
||||||
const project = modrinthProjects.find((x) => file.metadata.project_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)
|
const version = modrinthVersions.find((x) => file.metadata?.version_id === x.id)
|
||||||
|
|
||||||
if (project && version) {
|
if (project && version) {
|
||||||
const org = project.organization
|
const org = project.organization
|
||||||
@@ -420,7 +401,7 @@ const initProjects = async (cacheBehaviour?) => {
|
|||||||
|
|
||||||
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
|
const team = modrinthTeams.find((x) => x[0].team_id === project.team)
|
||||||
|
|
||||||
let author: ProjectListEntryAuthor | null
|
let author: ProjectListEntryAuthor | null = null
|
||||||
if (org) {
|
if (org) {
|
||||||
author = {
|
author = {
|
||||||
name: org.name,
|
name: org.name,
|
||||||
@@ -429,13 +410,13 @@ const initProjects = async (cacheBehaviour?) => {
|
|||||||
}
|
}
|
||||||
} else if (team) {
|
} else if (team) {
|
||||||
const teamMember = team.find((x) => x.is_owner)
|
const teamMember = team.find((x) => x.is_owner)
|
||||||
author = {
|
if (teamMember) {
|
||||||
name: teamMember.user.username,
|
author = {
|
||||||
slug: teamMember.user.username,
|
name: teamMember.user.username,
|
||||||
type: 'user',
|
slug: teamMember.user.username,
|
||||||
|
type: 'user',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
author = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newProjects.push({
|
newProjects.push({
|
||||||
@@ -464,7 +445,7 @@ const initProjects = async (cacheBehaviour?) => {
|
|||||||
author: null,
|
author: null,
|
||||||
version: null,
|
version: null,
|
||||||
file_name: file.file_name,
|
file_name: file.file_name,
|
||||||
icon: null,
|
icon: undefined,
|
||||||
disabled: file.file_name.endsWith('.disabled'),
|
disabled: file.file_name.endsWith('.disabled'),
|
||||||
outdated: false,
|
outdated: false,
|
||||||
updated: dayjs(0),
|
updated: dayjs(0),
|
||||||
@@ -488,7 +469,7 @@ const initProjects = async (cacheBehaviour?) => {
|
|||||||
}
|
}
|
||||||
await initProjects()
|
await initProjects()
|
||||||
|
|
||||||
const modpackVersionModal = ref(null)
|
const modpackVersionModal = ref<InstanceType<typeof ModpackVersionModal> | null>()
|
||||||
const installing = computed(() => props.instance.install_stage !== 'installed')
|
const installing = computed(() => props.instance.install_stage !== 'installed')
|
||||||
|
|
||||||
const vintl = useVIntl()
|
const vintl = useVIntl()
|
||||||
@@ -513,7 +494,7 @@ const messages = defineMessages({
|
|||||||
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
||||||
const options: FilterOption[] = []
|
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
|
map[item.project_type] = (map[item.project_type] || 0) + 1
|
||||||
return map
|
return map
|
||||||
}, {})
|
}, {})
|
||||||
@@ -544,7 +525,7 @@ const filterOptions: ComputedRef<FilterOption[]> = computed(() => {
|
|||||||
return options
|
return options
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedFilters = ref([])
|
const selectedFilters = ref<string[]>([])
|
||||||
const filteredProjects = computed(() => {
|
const filteredProjects = computed(() => {
|
||||||
const updatesFilter = selectedFilters.value.includes('updates')
|
const updatesFilter = selectedFilters.value.includes('updates')
|
||||||
const disabledFilter = selectedFilters.value.includes('disabled')
|
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)) {
|
if (array.includes(value)) {
|
||||||
array.splice(array.indexOf(value), 1)
|
array.splice(array.indexOf(value), 1)
|
||||||
} else {
|
} else {
|
||||||
@@ -581,7 +562,7 @@ function toggleArray(array, value) {
|
|||||||
|
|
||||||
const searchFilter = ref('')
|
const searchFilter = ref('')
|
||||||
const selectAll = ref(false)
|
const selectAll = ref(false)
|
||||||
const shareModal = ref(null)
|
const shareModal = ref<InstanceType<typeof ShareModalWrapper> | null>()
|
||||||
const ascending = ref(true)
|
const ascending = ref(true)
|
||||||
const sortColumn = ref('Name')
|
const sortColumn = ref('Name')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@@ -622,7 +603,7 @@ const search = computed(() => {
|
|||||||
|
|
||||||
watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1))
|
watch([sortColumn, ascending, selectedFilters.value, searchFilter], () => (currentPage.value = 1))
|
||||||
|
|
||||||
const sortProjects = (filter) => {
|
const sortProjects = (filter: string) => {
|
||||||
if (sortColumn.value === filter) {
|
if (sortColumn.value === filter) {
|
||||||
ascending.value = !ascending.value
|
ascending.value = !ascending.value
|
||||||
} else {
|
} 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)) {
|
for (const [oldVal, newVal] of Object.entries(paths)) {
|
||||||
const index = projects.value.findIndex((x) => x.path === oldVal)
|
const index = projects.value.findIndex((x) => x.path === oldVal)
|
||||||
@@ -649,7 +630,7 @@ const updateAll = async () => {
|
|||||||
|
|
||||||
if (projects.value[index].updateVersion) {
|
if (projects.value[index].updateVersion) {
|
||||||
projects.value[index].version = projects.value[index].updateVersion.version_number
|
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) {
|
for (const project of setProjects) {
|
||||||
@@ -664,15 +645,15 @@ const updateAll = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateProject = async (mod) => {
|
const updateProject = async (mod: ProjectListEntry) => {
|
||||||
mod.updating = true
|
mod.updating = true
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||||
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
|
mod.path = await update_project(props.instance.path, mod.path).catch(handleError)
|
||||||
mod.updating = false
|
mod.updating = false
|
||||||
|
|
||||||
mod.outdated = false
|
mod.outdated = false
|
||||||
mod.version = mod.updateVersion.version_number
|
mod.version = mod.updateVersion?.version_number
|
||||||
mod.updateVersion = null
|
mod.updateVersion = undefined
|
||||||
|
|
||||||
trackEvent('InstanceProjectUpdate', {
|
trackEvent('InstanceProjectUpdate', {
|
||||||
loader: props.instance.loader,
|
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.
|
// 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]
|
const lock = locks[mod.file_name]
|
||||||
|
|
||||||
while (lock) {
|
while (lock) {
|
||||||
await new Promise((resolve) => {
|
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
|
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)
|
await remove_project(props.instance.path, mod.path).catch(handleError)
|
||||||
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
projects.value = projects.value.filter((x) => mod.path !== x.path)
|
||||||
|
|
||||||
trackEvent('InstanceProjectRemove', {
|
trackEvent('InstanceProjectRemove', {
|
||||||
loader: props.instance.loader,
|
loader: props.instance.loader,
|
||||||
game_version: props.instance.game_version,
|
game_version: props.instance.game_version,
|
||||||
id: mod.id,
|
id: mod.data.id,
|
||||||
name: mod.name,
|
name: mod.data.name,
|
||||||
project_type: mod.project_type,
|
project_type: mod.data.project_type,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyModLink = async (mod) => {
|
const copyModLink = async (mod: ContentItem<ProjectListEntry>) => {
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
`https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`,
|
`https://modrinth.com/${mod.data.project_type}/${mod.data.slug}`,
|
||||||
)
|
)
|
||||||
@@ -744,15 +725,15 @@ const deleteSelected = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shareNames = 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 () => {
|
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 () => {
|
const shareUrls = async () => {
|
||||||
await shareModal.value.show(
|
await shareModal.value?.show(
|
||||||
functionValues.value
|
functionValues.value
|
||||||
.filter((x) => x.slug)
|
.filter((x) => x.slug)
|
||||||
.map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`)
|
.map((x) => `https://modrinth.com/${x.project_type}/${x.slug}`)
|
||||||
@@ -761,7 +742,7 @@ const shareUrls = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shareMarkdown = async () => {
|
const shareMarkdown = async () => {
|
||||||
await shareModal.value.show(
|
await shareModal.value?.show(
|
||||||
functionValues.value
|
functionValues.value
|
||||||
.map((x) => {
|
.map((x) => {
|
||||||
if (x.slug) {
|
if (x.slug) {
|
||||||
@@ -826,15 +807,17 @@ const unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
|
|||||||
await initProjects()
|
await initProjects()
|
||||||
})
|
})
|
||||||
|
|
||||||
const unlistenProfiles = await profile_listener(async (event) => {
|
const unlistenProfiles = await profile_listener(
|
||||||
if (
|
async (event: { event: string; profile_path_id: string }) => {
|
||||||
event.profile_path_id === props.instance.path &&
|
if (
|
||||||
event.event === 'synced' &&
|
event.profile_path_id === props.instance.path &&
|
||||||
props.instance.install_stage !== 'pack_installing'
|
event.event === 'synced' &&
|
||||||
) {
|
props.instance.install_stage !== 'pack_installing'
|
||||||
await initProjects()
|
) {
|
||||||
}
|
await initProjects()
|
||||||
})
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlisten()
|
unlisten()
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</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">
|
<div class="flex flex-col w-full gap-2">
|
||||||
<WorldItem
|
<WorldItem
|
||||||
v-for="world in filteredWorlds"
|
v-for="world in filteredWorlds"
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
world.type === 'server' ? editServerModal?.show(world) : editWorldModal?.show(world)
|
||||||
"
|
"
|
||||||
@delete="() => promptToRemoveWorld(world)"
|
@delete="() => promptToRemoveWorld(world)"
|
||||||
|
@open-folder="(world: SingleplayerWorld) => showWorldInFolder(instance.path, world.path)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,6 +152,7 @@ import {
|
|||||||
hasQuickPlaySupport,
|
hasQuickPlaySupport,
|
||||||
refreshWorlds,
|
refreshWorlds,
|
||||||
handleDefaultProfileUpdateEvent,
|
handleDefaultProfileUpdateEvent,
|
||||||
|
showWorldInFolder,
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||||
@@ -225,6 +227,11 @@ const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
|||||||
await refreshAllWorlds()
|
await refreshAllWorlds()
|
||||||
|
|
||||||
async function refreshServer(address: string) {
|
async function refreshServer(address: string) {
|
||||||
|
if (!serverData.value[address]) {
|
||||||
|
serverData.value[address] = {
|
||||||
|
refreshing: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
await refreshServerData(serverData.value[address], protocolVersion.value, address)
|
await refreshServerData(serverData.value[address], protocolVersion.value, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,9 +270,12 @@ async function addServer(server: ServerWorld) {
|
|||||||
async function editServer(server: ServerWorld) {
|
async function editServer(server: ServerWorld) {
|
||||||
const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index)
|
const index = worlds.value.findIndex((w) => w.type === 'server' && w.index === server.index)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
|
const oldServer = worlds.value[index] as ServerWorld
|
||||||
worlds.value[index] = server
|
worlds.value[index] = server
|
||||||
sortWorlds(worlds.value)
|
sortWorlds(worlds.value)
|
||||||
await refreshServer(server.address)
|
if (oldServer.address !== server.address) {
|
||||||
|
await refreshServer(server.address)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
handleError(`Error refreshing server, refreshing all worlds`)
|
handleError(`Error refreshing server, refreshing all worlds`)
|
||||||
await refreshAllWorlds()
|
await refreshAllWorlds()
|
||||||
@@ -349,26 +359,34 @@ const supportsQuickPlay = computed(() =>
|
|||||||
const filterOptions = computed(() => {
|
const filterOptions = computed(() => {
|
||||||
const options: FilterBarOption[] = []
|
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({
|
options.push({
|
||||||
id: 'singleplayer',
|
id: 'singleplayer',
|
||||||
message: messages.singleplayer,
|
message: messages.singleplayer,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (worlds.value.some((x) => x.type === 'server')) {
|
|
||||||
options.push({
|
options.push({
|
||||||
id: 'server',
|
id: 'server',
|
||||||
message: messages.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 (
|
if (
|
||||||
worlds.value.some(
|
worlds.value.some(
|
||||||
(x) =>
|
(x) =>
|
||||||
x.type === 'server' &&
|
x.type === 'server' &&
|
||||||
!serverData.value[x.address]?.status &&
|
!serverData.value[x.address]?.status &&
|
||||||
!serverData.value[x.address]?.refreshing,
|
!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({
|
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 { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ref, shallowRef, watch } from 'vue'
|
import { ref, shallowRef, watch } from 'vue'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
@@ -170,6 +170,7 @@ import { openUrl } from '@tauri-apps/plugin-opener'
|
|||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -247,6 +248,9 @@ async function install(version) {
|
|||||||
installedVersion.value = version
|
installedVersion.value = version
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
(profile) => {
|
||||||
|
router.push(`/instance/${profile}`)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export const useInstall = defineStore('installStore', {
|
|||||||
setInstallConfirmModal(ref) {
|
setInstallConfirmModal(ref) {
|
||||||
this.installConfirmModal = ref
|
this.installConfirmModal = ref
|
||||||
},
|
},
|
||||||
showInstallConfirmModal(project, version_id, onInstall) {
|
showInstallConfirmModal(project, version_id, onInstall, createInstanceCallback) {
|
||||||
this.installConfirmModal.show(project, version_id, onInstall)
|
this.installConfirmModal.show(project, version_id, onInstall, createInstanceCallback)
|
||||||
},
|
},
|
||||||
setIncompatibilityWarningModal(ref) {
|
setIncompatibilityWarningModal(ref) {
|
||||||
this.incompatibilityWarningModal = 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)
|
const project = await get_project(projectId, 'must_revalidate').catch(handleError)
|
||||||
|
|
||||||
if (project.project_type === 'modpack') {
|
if (project.project_type === 'modpack') {
|
||||||
@@ -49,7 +56,13 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
|||||||
const packs = await list().catch(handleError)
|
const packs = await list().catch(handleError)
|
||||||
|
|
||||||
if (packs.length === 0 || !packs.find((pack) => pack.linked_data?.project_id === project.id)) {
|
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', {
|
trackEvent('PackInstall', {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
@@ -61,7 +74,7 @@ export const install = async (projectId, versionId, instancePath, source, callba
|
|||||||
callback(version)
|
callback(version)
|
||||||
} else {
|
} else {
|
||||||
const install = useInstall()
|
const install = useInstall()
|
||||||
install.showInstallConfirmModal(project, version, callback)
|
install.showInstallConfirmModal(project, version, callback, createInstanceCallback)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (instancePath) {
|
if (instancePath) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTheming } from './theme'
|
import { useTheming } from './theme.ts'
|
||||||
import { useBreadcrumbs } from './breadcrumbs'
|
import { useBreadcrumbs } from './breadcrumbs'
|
||||||
import { useLoading } from './loading'
|
import { useLoading } from './loading'
|
||||||
import { useNotifications, handleError } from './notifications'
|
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]
|
[package]
|
||||||
name = "theseus_playground"
|
name = "theseus_playground"
|
||||||
version = "0.0.0"
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
theseus = { path = "../../packages/app-lib", features = ["cli"] }
|
theseus = { workspace = true, features = ["cli"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
webbrowser = "0.8.13"
|
enumset.workspace = true
|
||||||
|
|
||||||
tracing = "0.1.37"
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
"name": "@modrinth/app-playground",
|
"name": "@modrinth/app-playground",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix",
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo test"
|
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
use enumset::EnumSet;
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
use theseus::worlds::get_recent_worlds;
|
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.");
|
println!("A browser window will now open, follow the login flow there.");
|
||||||
let login = minecraft_auth::begin_login().await?;
|
let login = minecraft_auth::begin_login().await?;
|
||||||
|
|
||||||
println!("URL {}", login.redirect_uri.as_str());
|
println!("Open URL {} in a browser", login.redirect_uri.as_str());
|
||||||
webbrowser::open(login.redirect_uri.as_str())?;
|
|
||||||
|
|
||||||
println!("Please enter URL code: ");
|
println!("Please enter URL code: ");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
@@ -27,7 +27,10 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
|||||||
|
|
||||||
let credentials = minecraft_auth::finish_login(&input, login).await?;
|
let credentials = minecraft_auth::finish_login(&input, login).await?;
|
||||||
|
|
||||||
println!("Logged in user {}.", credentials.username);
|
println!(
|
||||||
|
"Logged in user {}.",
|
||||||
|
credentials.maybe_online_profile().await.name
|
||||||
|
);
|
||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +43,7 @@ async fn main() -> theseus::Result<()> {
|
|||||||
// Initialize state
|
// Initialize state
|
||||||
State::init().await?;
|
State::init().await?;
|
||||||
|
|
||||||
let worlds = get_recent_worlds(4).await?;
|
let worlds = get_recent_worlds(4, EnumSet::all()).await?;
|
||||||
for world in worlds {
|
for world in worlds {
|
||||||
println!(
|
println!(
|
||||||
"World: {:?}/{:?} played at {:?}: {:#?}",
|
"World: {:?}/{:?} played at {:?}: {:#?}",
|
||||||
|
|||||||
@@ -1,62 +1,51 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.9.4"
|
version = "0.9.5"
|
||||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
repository = "https://github.com/modrinth/code/apps/app/"
|
repository = "https://github.com/modrinth/code/apps/app/"
|
||||||
edition = "2021"
|
edition.workspace = true
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0.3", features = ["codegen"] }
|
tauri-build = { workspace = true, features = ["codegen"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
theseus = { path = "../../packages/app-lib", features = ["tauri"] }
|
theseus = { workspace = true, features = ["tauri"] }
|
||||||
|
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_with = "3.0.0"
|
serde_with.workspace = true
|
||||||
|
|
||||||
tauri = { version = "2.1.1", features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
tauri = { workspace = true, features = ["devtools", "macos-private-api", "protocol-asset", "unstable"] }
|
||||||
tauri-plugin-window-state = "2.2.0"
|
tauri-plugin-window-state.workspace = true
|
||||||
tauri-plugin-deep-link = "2.2.0"
|
tauri-plugin-deep-link.workspace = true
|
||||||
tauri-plugin-os = "2.2.0"
|
tauri-plugin-os.workspace = true
|
||||||
tauri-plugin-opener = "2.2.1"
|
tauri-plugin-opener.workspace = true
|
||||||
tauri-plugin-dialog = "2.2.0"
|
tauri-plugin-dialog.workspace = true
|
||||||
tauri-plugin-updater = { version = "2.3.0" }
|
tauri-plugin-updater.workspace = true
|
||||||
tauri-plugin-single-instance = { version = "2.2.0" }
|
tauri-plugin-single-instance.workspace = true
|
||||||
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { workspace = true, features = ["time"] }
|
||||||
thiserror = "1.0"
|
thiserror.workspace = true
|
||||||
daedalus = { path = "../../packages/daedalus" }
|
daedalus.workspace = true
|
||||||
chrono = "0.4.26"
|
chrono.workspace = true
|
||||||
either = "1.15"
|
either.workspace = true
|
||||||
|
|
||||||
url = "2.2"
|
url.workspace = true
|
||||||
urlencoding = "2.1"
|
urlencoding.workspace = true
|
||||||
uuid = { version = "1.1", features = ["serde", "v4"] }
|
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||||
os_info = "3.7.0"
|
|
||||||
|
|
||||||
tracing = "0.1.37"
|
tracing.workspace = true
|
||||||
tracing-error = "0.2.0"
|
tracing-error.workspace = true
|
||||||
|
|
||||||
dashmap = "6.0.1"
|
dashmap.workspace = true
|
||||||
paste = "1.0.15"
|
paste.workspace = true
|
||||||
|
enumset = { workspace = true, features = ["serde"] }
|
||||||
|
|
||||||
opener = { version = "0.7.2", features = ["reveal", "dbus-vendored"] }
|
native-dialog.workspace = true
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[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]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
@@ -66,3 +55,6 @@ default = ["custom-protocol"]
|
|||||||
# DO NOT remove this
|
# DO NOT remove this
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
updater = []
|
updater = []
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ fn main() {
|
|||||||
"profile_update_managed_modrinth_version",
|
"profile_update_managed_modrinth_version",
|
||||||
"profile_repair_managed_modrinth",
|
"profile_repair_managed_modrinth",
|
||||||
"profile_run",
|
"profile_run",
|
||||||
"profile_run_credentials",
|
|
||||||
"profile_kill",
|
"profile_kill",
|
||||||
"profile_edit",
|
"profile_edit",
|
||||||
"profile_edit_icon",
|
"profile_edit_icon",
|
||||||
@@ -248,6 +247,7 @@ fn main() {
|
|||||||
"get_recent_worlds",
|
"get_recent_worlds",
|
||||||
"get_profile_worlds",
|
"get_profile_worlds",
|
||||||
"get_singleplayer_world",
|
"get_singleplayer_world",
|
||||||
|
"set_world_display_status",
|
||||||
"rename_world",
|
"rename_world",
|
||||||
"reset_world_icon",
|
"reset_world_icon",
|
||||||
"backup_world",
|
"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",
|
"name": "@modrinth/app",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tauri build",
|
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
|
"build": "tauri build",
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"test": "cargo test",
|
"test": "cargo nextest run --all-targets --no-fail-fast",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix"
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "2.1.0"
|
"@tauri-apps/cli": "2.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/app-frontend": "workspace:*",
|
"@modrinth/app-frontend": "workspace:*",
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
|||||||
profile_update_managed_modrinth_version,
|
profile_update_managed_modrinth_version,
|
||||||
profile_repair_managed_modrinth,
|
profile_repair_managed_modrinth,
|
||||||
profile_run,
|
profile_run,
|
||||||
profile_run_credentials,
|
|
||||||
profile_kill,
|
profile_kill,
|
||||||
profile_edit,
|
profile_edit,
|
||||||
profile_edit_icon,
|
profile_edit_icon,
|
||||||
@@ -256,22 +255,6 @@ pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
|||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run Minecraft using a profile using chosen credentials
|
|
||||||
// Returns the UUID, which can be used to poll
|
|
||||||
// for the actual Child in the state.
|
|
||||||
// invoke('plugin:profile|profile_run_credentials', {path, credentials})')
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn profile_run_credentials(
|
|
||||||
path: &str,
|
|
||||||
credentials: Credentials,
|
|
||||||
) -> Result<ProcessMetadata> {
|
|
||||||
let process =
|
|
||||||
profile::run_credentials(path, &credentials, &QuickPlayType::None)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(process)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_kill(path: &str) -> Result<()> {
|
pub async fn profile_kill(path: &str) -> Result<()> {
|
||||||
profile::kill(path).await?;
|
profile::kill(path).await?;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::Runtime;
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
use theseus::{
|
use theseus::{
|
||||||
handler,
|
handler,
|
||||||
prelude::{CommandPayload, DirectoryInfo},
|
prelude::{CommandPayload, DirectoryInfo},
|
||||||
@@ -35,6 +37,7 @@ pub fn get_os() -> OS {
|
|||||||
let os = OS::MacOS;
|
let os = OS::MacOS;
|
||||||
os
|
os
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub enum OS {
|
pub enum OS {
|
||||||
@@ -47,57 +50,56 @@ pub enum OS {
|
|||||||
// Create a new HashMap with the same keys
|
// 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
|
// Values provided should not be used directly, as they are not guaranteed to be up-to-date
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn progress_bars_list(
|
pub async fn progress_bars_list()
|
||||||
) -> Result<DashMap<uuid::Uuid, theseus::LoadingBar>> {
|
-> Result<DashMap<uuid::Uuid, theseus::LoadingBar>> {
|
||||||
let res = theseus::EventState::list_progress_bars().await?;
|
let res = theseus::EventState::list_progress_bars().await?;
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cfg only on mac os
|
|
||||||
// disables mouseover and fixes a random crash error only fixed by recent versions of macos
|
// disables mouseover and fixes a random crash error only fixed by recent versions of macos
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn should_disable_mouseover() -> bool {
|
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
|
if cfg!(target_os = "macos") {
|
||||||
let os = os_info::get();
|
// 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 os_info::Version::Semantic(major, minor, _) = os.version() {
|
if let tauri_plugin_os::Version::Semantic(major, minor, _) =
|
||||||
if *major >= 12 && *minor >= 3 {
|
tauri_plugin_os::version()
|
||||||
// Mac os version is 12.3 or higher, we allow mouseover
|
{
|
||||||
return false;
|
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]
|
#[tauri::command]
|
||||||
pub fn highlight_in_folder(path: PathBuf) {
|
pub fn highlight_in_folder<R: Runtime>(
|
||||||
let res = opener::reveal(path);
|
app: tauri::AppHandle<R>,
|
||||||
|
path: PathBuf,
|
||||||
if let Err(e) = res {
|
) {
|
||||||
|
if let Err(e) = app.opener().reveal_item_in_dir(path) {
|
||||||
tracing::error!("Failed to highlight file in folder: {}", e);
|
tracing::error!("Failed to highlight file in folder: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn open_path(path: PathBuf) {
|
pub fn open_path<R: Runtime>(app: tauri::AppHandle<R>, path: PathBuf) {
|
||||||
let res = opener::open(path);
|
if let Err(e) = app.opener().open_path(path.to_string_lossy(), None::<&str>)
|
||||||
|
{
|
||||||
if let Err(e) = res {
|
|
||||||
tracing::error!("Failed to open path: {}", e);
|
tracing::error!("Failed to open path: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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();
|
let path = DirectoryInfo::launcher_logs_dir().unwrap_or_default();
|
||||||
// failure to get folder just opens filesystem
|
// failure to get folder just opens filesystem
|
||||||
// (ie: if in debug mode only and launcher_logs never created)
|
// (ie: if in debug mode only and launcher_logs never created)
|
||||||
open_path(path);
|
open_path(app, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get opening command
|
// Get opening command
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use crate::api::Result;
|
use crate::api::Result;
|
||||||
use either::Either;
|
use either::Either;
|
||||||
|
use enumset::EnumSet;
|
||||||
use tauri::{AppHandle, Manager, Runtime};
|
use tauri::{AppHandle, Manager, Runtime};
|
||||||
use theseus::prelude::ProcessMetadata;
|
use theseus::prelude::ProcessMetadata;
|
||||||
use theseus::profile::{get_full_path, QuickPlayType};
|
use theseus::profile::{QuickPlayType, get_full_path};
|
||||||
use theseus::worlds::{
|
use theseus::worlds::{
|
||||||
ServerPackStatus, ServerStatus, World, WorldWithProfile,
|
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
||||||
|
WorldWithProfile,
|
||||||
};
|
};
|
||||||
use theseus::{profile, worlds};
|
use theseus::{profile, worlds};
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
|||||||
get_recent_worlds,
|
get_recent_worlds,
|
||||||
get_profile_worlds,
|
get_profile_worlds,
|
||||||
get_singleplayer_world,
|
get_singleplayer_world,
|
||||||
|
set_world_display_status,
|
||||||
rename_world,
|
rename_world,
|
||||||
reset_world_icon,
|
reset_world_icon,
|
||||||
backup_world,
|
backup_world,
|
||||||
@@ -33,9 +36,14 @@ pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
|||||||
pub async fn get_recent_worlds<R: Runtime>(
|
pub async fn get_recent_worlds<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
display_statuses: Option<EnumSet<DisplayStatus>>,
|
||||||
) -> Result<Vec<WorldWithProfile>> {
|
) -> Result<Vec<WorldWithProfile>> {
|
||||||
let mut result = worlds::get_recent_worlds(limit).await?;
|
let mut result = worlds::get_recent_worlds(
|
||||||
for world in result.iter_mut() {
|
limit,
|
||||||
|
display_statuses.unwrap_or(EnumSet::all()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
for world in &mut result {
|
||||||
adapt_world_icon(&app_handle, &mut world.world);
|
adapt_world_icon(&app_handle, &mut world.world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -47,7 +55,7 @@ pub async fn get_profile_worlds<R: Runtime>(
|
|||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<Vec<World>> {
|
) -> Result<Vec<World>> {
|
||||||
let mut result = worlds::get_profile_worlds(path).await?;
|
let mut result = worlds::get_profile_worlds(path).await?;
|
||||||
for world in result.iter_mut() {
|
for world in &mut result {
|
||||||
adapt_world_icon(&app_handle, world);
|
adapt_world_icon(&app_handle, world);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -59,8 +67,7 @@ pub async fn get_singleplayer_world<R: Runtime>(
|
|||||||
instance: &str,
|
instance: &str,
|
||||||
world: &str,
|
world: &str,
|
||||||
) -> Result<World> {
|
) -> 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);
|
adapt_world_icon(&app_handle, &mut world);
|
||||||
Ok(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]
|
#[tauri::command]
|
||||||
pub async fn rename_world(
|
pub async fn rename_world(
|
||||||
instance: &str,
|
instance: &str,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager: &M,
|
manager: &M,
|
||||||
) -> InitialPayload {
|
) -> InitialPayload {
|
||||||
let initial_payload = manager.try_state::<InitialPayload>();
|
let initial_payload = manager.try_state::<InitialPayload>();
|
||||||
let mtx = if let Some(initial_payload) = initial_payload {
|
|
||||||
|
if let Some(initial_payload) = initial_payload {
|
||||||
initial_payload.inner().clone()
|
initial_payload.inner().clone()
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("No initial payload found, creating new");
|
tracing::info!("No initial payload found, creating new");
|
||||||
@@ -22,7 +23,5 @@ pub fn get_or_init_payload<R: Runtime, M: Manager<R>>(
|
|||||||
manager.manage(payload.clone());
|
manager.manage(payload.clone());
|
||||||
|
|
||||||
payload
|
payload
|
||||||
};
|
}
|
||||||
|
|
||||||
mtx
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
pub mod deep_link;
|
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"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use native_dialog::{MessageDialog, MessageType};
|
use native_dialog::{DialogBuilder, MessageLevel};
|
||||||
use std::env;
|
use std::env;
|
||||||
use tauri::{Listener, Manager};
|
use tauri::{Listener, Manager};
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
@@ -14,14 +14,6 @@ mod error;
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
mod 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
|
// Should be called in launcher initialization
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -113,14 +105,14 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
|||||||
fn show_window(app: tauri::AppHandle) {
|
fn show_window(app: tauri::AppHandle) {
|
||||||
let win = app.get_window("main").unwrap();
|
let win = app.get_window("main").unwrap();
|
||||||
if let Err(e) = win.show() {
|
if let Err(e) = win.show() {
|
||||||
MessageDialog::new()
|
DialogBuilder::message()
|
||||||
.set_type(MessageType::Error)
|
.set_level(MessageLevel::Error)
|
||||||
.set_title("Initialization error")
|
.set_title("Initialization error")
|
||||||
.set_text(&format!(
|
.set_text(format!(
|
||||||
"Cannot display application window due to an error:\n{}",
|
"Cannot display application window due to an error:\n{e}"
|
||||||
e
|
|
||||||
))
|
))
|
||||||
.show_alert()
|
.alert()
|
||||||
|
.show()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
panic!("cannot display application window")
|
panic!("cannot display application window")
|
||||||
} else {
|
} else {
|
||||||
@@ -138,8 +130,7 @@ fn is_dev() -> bool {
|
|||||||
async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
|
async fn toggle_decorations(b: bool, window: tauri::Window) -> api::Result<()> {
|
||||||
window.set_decorations(b).map_err(|e| {
|
window.set_decorations(b).map_err(|e| {
|
||||||
theseus::Error::from(theseus::ErrorKind::OtherError(format!(
|
theseus::Error::from(theseus::ErrorKind::OtherError(format!(
|
||||||
"Failed to toggle decorations: {}",
|
"Failed to toggle decorations: {e}"
|
||||||
e
|
|
||||||
)))
|
)))
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -206,7 +197,7 @@ fn main() {
|
|||||||
{
|
{
|
||||||
let payload = macos::deep_link::get_or_init_payload(app);
|
let payload = macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload.clone();
|
let mtx_copy = payload.payload;
|
||||||
app.listen("deep-link://new-url", move |url| {
|
app.listen("deep-link://new-url", move |url| {
|
||||||
let mtx_copy_copy = mtx_copy.clone();
|
let mtx_copy_copy = mtx_copy.clone();
|
||||||
let request = url.payload().to_owned();
|
let request = url.payload().to_owned();
|
||||||
@@ -238,13 +229,12 @@ fn main() {
|
|||||||
tauri::async_runtime::spawn(api::utils::handle_command(
|
tauri::async_runtime::spawn(api::utils::handle_command(
|
||||||
payload,
|
payload,
|
||||||
));
|
));
|
||||||
dbg!(url);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
{
|
if let Some(window) = app.get_window("main") {
|
||||||
if let Some(window) = app.get_window("main") {
|
if let Err(e) = window.set_shadow(true) {
|
||||||
window.set_shadow(true).unwrap();
|
tracing::warn!("Failed to set window shadow: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,32 +267,27 @@ fn main() {
|
|||||||
restart_app,
|
restart_app,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
builder = builder.plugin(macos::window_ext::init());
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!("Initializing app...");
|
tracing::info!("Initializing app...");
|
||||||
let app = builder.build(tauri::generate_context!());
|
let app = builder.build(tauri::generate_context!());
|
||||||
|
|
||||||
match app {
|
match app {
|
||||||
Ok(app) => {
|
Ok(app) => {
|
||||||
#[allow(unused_variables)]
|
|
||||||
app.run(|app, event| {
|
app.run(|app, event| {
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
drop((app, event));
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let tauri::RunEvent::Opened { urls } = event {
|
if let tauri::RunEvent::Opened { urls } = event {
|
||||||
tracing::info!("Handling webview open {urls:?}");
|
tracing::info!("Handling webview open {urls:?}");
|
||||||
|
|
||||||
let file = urls
|
let file = urls
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|url| url.to_file_path().ok())
|
.find_map(|url| url.to_file_path().ok());
|
||||||
.next();
|
|
||||||
|
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
let payload =
|
let payload =
|
||||||
macos::deep_link::get_or_init_payload(app);
|
macos::deep_link::get_or_init_payload(app);
|
||||||
|
|
||||||
let mtx_copy = payload.payload.clone();
|
let mtx_copy = payload.payload;
|
||||||
let request = file.to_string_lossy().to_string();
|
let request = file.to_string_lossy().to_string();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mut payload = mtx_copy.lock().await;
|
let mut payload = mtx_copy.lock().await;
|
||||||
@@ -320,28 +305,29 @@ fn main() {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
// tauri doesn't expose runtime errors, so matching a string representation seems like the only solution
|
// 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",
|
"Runtime(CreateWebview(WebView2Error(WindowsError",
|
||||||
) {
|
) {
|
||||||
MessageDialog::new()
|
DialogBuilder::message()
|
||||||
.set_type(MessageType::Error)
|
.set_level(MessageLevel::Error)
|
||||||
.set_title("Initialization 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")
|
.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();
|
.unwrap();
|
||||||
|
|
||||||
panic!("webview2 initialization failed")
|
panic!("webview2 initialization failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog::new()
|
DialogBuilder::message()
|
||||||
.set_type(MessageType::Error)
|
.set_level(MessageLevel::Error)
|
||||||
.set_title("Initialization error")
|
.set_title("Initialization error")
|
||||||
.set_text(&format!(
|
.set_text(format!(
|
||||||
"Cannot initialize application due to an error:\n{:?}",
|
"Cannot initialize application due to an error:\n{e:?}"
|
||||||
e
|
|
||||||
))
|
))
|
||||||
.show_alert()
|
.alert()
|
||||||
|
.show()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
tracing::error!("Error while running tauri application: {:?}", e);
|
tracing::error!("Error while running tauri application: {:?}", e);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm turbo run dev --filter=@modrinth/app-frontend",
|
"beforeDevCommand": "pnpm turbo run dev --filter=@modrinth/app-frontend",
|
||||||
"beforeBuildCommand": "pnpm turbo run build --filter=@modrinth/app-frontend",
|
"beforeBuildCommand": "pnpm turbo run build --filter=@modrinth/app-frontend",
|
||||||
@@ -44,7 +45,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"productName": "Modrinth App",
|
"productName": "Modrinth App",
|
||||||
"version": "0.9.4",
|
"version": "0.9.5",
|
||||||
"mainBinaryName": "Modrinth App",
|
"mainBinaryName": "Modrinth App",
|
||||||
"identifier": "ModrinthApp",
|
"identifier": "ModrinthApp",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
@@ -76,18 +77,26 @@
|
|||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"assetProtocol": {
|
"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
|
"enable": true
|
||||||
},
|
},
|
||||||
"capabilities": ["ads", "core", "plugins"],
|
"capabilities": ["ads", "core", "plugins"],
|
||||||
"csp": {
|
"csp": {
|
||||||
"default-src": "'self' customprotocol: asset:",
|
"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",
|
"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:",
|
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost blob: data:",
|
||||||
"style-src": "'unsafe-inline' 'self'",
|
"style-src": "'unsafe-inline' 'self'",
|
||||||
"script-src": "https://*.posthog.com '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,
|
"minWidth": 1100,
|
||||||
"visible": false,
|
"visible": false,
|
||||||
"zoomHotkeysEnabled": 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"
|
name = "daedalus_client"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
authors = ["Jai A <jai@modrinth.com>"]
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
daedalus = { path = "../../packages/daedalus" }
|
daedalus.workspace = true
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] }
|
||||||
futures = "0.3.25"
|
futures.workspace = true
|
||||||
dotenvy = "0.15.6"
|
dotenvy.workspace = true
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json.workspace = true
|
||||||
serde-xml-rs = "0.6.0"
|
serde-xml-rs.workspace = true
|
||||||
lazy_static = "1.4.0"
|
thiserror.workspace = true
|
||||||
thiserror = "1.0"
|
reqwest = { workspace = true, features = ["stream", "json", "rustls-tls-native-roots"] }
|
||||||
reqwest = { version = "0.12.5", default-features = false, features = [
|
async_zip = { workspace = true, features = ["chrono", "tokio-fs", "deflate", "bzip2", "zstd", "deflate64"] }
|
||||||
"stream",
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
"json",
|
bytes.workspace = true
|
||||||
"rustls-tls-native-roots",
|
rust-s3.workspace = true
|
||||||
] }
|
dashmap.workspace = true
|
||||||
async_zip = { version = "0.0.17", features = ["full"] }
|
sha1_smol.workspace = true
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
indexmap = { workspace = true, features = ["serde"] }
|
||||||
bytes = "1.6.0"
|
itertools.workspace = true
|
||||||
rust-s3 = { version = "0.33.0", default-features = false, features = [
|
tracing-error.workspace = true
|
||||||
"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"
|
|
||||||
|
|
||||||
tracing = "0.1"
|
tracing.workspace = true
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
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
|
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||||
|
|
||||||
WORKDIR /usr/src/daedalus
|
WORKDIR /usr/src/daedalus
|
||||||
@@ -9,9 +9,9 @@ RUN cargo build --release --package daedalus_client
|
|||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN update-ca-certificates
|
RUN update-ca-certificates
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
"name": "@modrinth/daedalus_client",
|
"name": "@modrinth/daedalus_client",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cargo build --release",
|
"build": "cargo build --release",
|
||||||
"lint": "cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings",
|
"lint": "cargo fmt --check && cargo clippy --all-targets",
|
||||||
"fix": "cargo fmt && cargo clippy --fix",
|
"fix": "cargo clippy --all-targets --fix --allow-dirty && cargo fmt",
|
||||||
"dev": "cargo run",
|
"dev": "cargo run",
|
||||||
"test": "cargo test"
|
"test": "cargo nextest run --all-targets --no-fail-fast"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth/daedalus": "workspace:*"
|
"@modrinth/daedalus": "workspace:*"
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ pub enum ErrorKind {
|
|||||||
SerdeJSON(#[from] serde_json::Error),
|
SerdeJSON(#[from] serde_json::Error),
|
||||||
#[error("Error while deserializing XML: {0}")]
|
#[error("Error while deserializing XML: {0}")]
|
||||||
SerdeXML(#[from] serde_xml_rs::Error),
|
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 {
|
ChecksumFailure {
|
||||||
hash: String,
|
hash: String,
|
||||||
url: String,
|
url: String,
|
||||||
@@ -22,7 +24,7 @@ pub enum ErrorKind {
|
|||||||
Fetch { inner: reqwest::Error, item: String },
|
Fetch { inner: reqwest::Error, item: String },
|
||||||
#[error("Error while uploading file to S3: {file}")]
|
#[error("Error while uploading file to S3: {file}")]
|
||||||
S3 {
|
S3 {
|
||||||
inner: s3::error::S3Error,
|
inner: Box<s3::error::S3Error>,
|
||||||
file: String,
|
file: String,
|
||||||
},
|
},
|
||||||
#[error("Error acquiring semaphore: {0}")]
|
#[error("Error acquiring semaphore: {0}")]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::util::{download_file, fetch_json, format_url};
|
use crate::util::{download_file, fetch_json, format_url};
|
||||||
use crate::{insert_mirrored_artifact, Error, MirrorArtifact, UploadFile};
|
use crate::{Error, MirrorArtifact, UploadFile, insert_mirrored_artifact};
|
||||||
use daedalus::modded::{Manifest, PartialVersionInfo, DUMMY_REPLACE_STRING};
|
use daedalus::modded::{DUMMY_REPLACE_STRING, Manifest, PartialVersionInfo};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -169,10 +169,11 @@ async fn fetch(
|
|||||||
insert_mirrored_artifact(
|
insert_mirrored_artifact(
|
||||||
&new_name,
|
&new_name,
|
||||||
None,
|
None,
|
||||||
vec![lib
|
vec![
|
||||||
.url
|
lib.url
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| maven_url.to_string())],
|
.unwrap_or_else(|| maven_url.to_string()),
|
||||||
|
],
|
||||||
false,
|
false,
|
||||||
mirror_artifacts,
|
mirror_artifacts,
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::util::{download_file, fetch_json, fetch_xml, format_url};
|
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 chrono::{DateTime, Utc};
|
||||||
use daedalus::get_path_from_artifact;
|
use daedalus::get_path_from_artifact;
|
||||||
use daedalus::modded::PartialVersionInfo;
|
use daedalus::modded::PartialVersionInfo;
|
||||||
@@ -7,8 +7,8 @@ use dashmap::DashMap;
|
|||||||
use futures::io::Cursor;
|
use futures::io::Cursor;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
@@ -57,7 +57,7 @@ pub async fn fetch_forge(
|
|||||||
|
|
||||||
ForgeVersion {
|
ForgeVersion {
|
||||||
format_version,
|
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,
|
raw: loader_version,
|
||||||
loader_version: version_split,
|
loader_version: version_split,
|
||||||
game_version: game_version.clone(),
|
game_version: game_version.clone(),
|
||||||
@@ -137,7 +137,7 @@ pub async fn fetch_neo(
|
|||||||
|
|
||||||
Ok(ForgeVersion {
|
Ok(ForgeVersion {
|
||||||
format_version: 2,
|
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,
|
raw: loader_version,
|
||||||
loader_version: version_split,
|
loader_version: version_split,
|
||||||
game_version: "1.20.1".to_string(), // All NeoForge Forge versions are for 1.20.1
|
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 {
|
Ok(ForgeVersion {
|
||||||
format_version: 2,
|
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(),
|
loader_version: loader_version.clone(),
|
||||||
raw: loader_version,
|
raw: loader_version,
|
||||||
game_version,
|
game_version,
|
||||||
@@ -502,7 +502,7 @@ async fn fetch(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
artifact.url =
|
artifact.url =
|
||||||
format_url(&format!("maven/{}", artifact_path));
|
format_url(&format!("maven/{artifact_path}"));
|
||||||
|
|
||||||
return Ok(lib);
|
return Ok(lib);
|
||||||
}
|
}
|
||||||
@@ -589,14 +589,16 @@ async fn fetch(
|
|||||||
mod_loader: &str,
|
mod_loader: &str,
|
||||||
version: &ForgeVersion,
|
version: &ForgeVersion,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let extract_file =
|
let extract_file = read_file(
|
||||||
read_file(zip, &value[1..value.len()])
|
zip,
|
||||||
.await?
|
&value[1..value.len()],
|
||||||
.ok_or_else(|| {
|
)
|
||||||
crate::ErrorKind::InvalidInput(format!(
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::InvalidInput(format!(
|
||||||
"Unable reading data key {key} at path {value}",
|
"Unable reading data key {key} at path {value}",
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let file_name = value.split('/').next_back()
|
let file_name = value.split('/').next_back()
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
@@ -622,10 +624,7 @@ async fn fetch(
|
|||||||
|
|
||||||
let path = format!(
|
let path = format!(
|
||||||
"com.modrinth.daedalus:{}-installer-extracts:{}:{}@{}",
|
"com.modrinth.daedalus:{}-installer-extracts:{}:{}@{}",
|
||||||
mod_loader,
|
mod_loader, version.raw, file_name, ext
|
||||||
version.raw,
|
|
||||||
file_name,
|
|
||||||
ext
|
|
||||||
);
|
);
|
||||||
|
|
||||||
upload_files.insert(
|
upload_files.insert(
|
||||||
@@ -753,7 +752,8 @@ async fn fetch(
|
|||||||
.rev()
|
.rev()
|
||||||
.chunk_by(|x| x.game_version.clone())
|
.chunk_by(|x| x.game_version.clone())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(game_version, loaders)| daedalus::modded::Version {
|
.map(|(game_version, loaders)| {
|
||||||
|
daedalus::modded::Version {
|
||||||
id: game_version,
|
id: game_version,
|
||||||
stable: true,
|
stable: true,
|
||||||
loaders: loaders
|
loaders: loaders
|
||||||
@@ -766,6 +766,7 @@ async fn fetch(
|
|||||||
stable: false,
|
stable: false,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use crate::util::{
|
use crate::util::{
|
||||||
format_url, upload_file_to_bucket, upload_url_to_bucket_mirrors,
|
REQWEST_CLIENT, format_url, upload_file_to_bucket,
|
||||||
REQWEST_CLIENT,
|
upload_url_to_bucket_mirrors,
|
||||||
};
|
};
|
||||||
use daedalus::get_path_from_artifact;
|
use daedalus::get_path_from_artifact;
|
||||||
use dashmap::{DashMap, DashSet};
|
use dashmap::{DashMap, DashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
use tracing_error::ErrorLayer;
|
use tracing_error::ErrorLayer;
|
||||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod fabric;
|
mod fabric;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::util::fetch_json;
|
use crate::util::fetch_json;
|
||||||
use crate::{
|
use crate::{
|
||||||
util::download_file, util::format_url, util::sha1_async, Error,
|
Error, MirrorArtifact, UploadFile, util::download_file, util::format_url,
|
||||||
MirrorArtifact, UploadFile,
|
util::sha1_async,
|
||||||
};
|
};
|
||||||
use daedalus::minecraft::{
|
use daedalus::minecraft::{
|
||||||
merge_partial_library, Library, PartialLibrary, VersionInfo,
|
Library, PartialLibrary, VERSION_MANIFEST_URL, VersionInfo,
|
||||||
VersionManifest, VERSION_MANIFEST_URL,
|
VersionManifest, merge_partial_library,
|
||||||
};
|
};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -52,8 +52,7 @@ pub async fn fetch(
|
|||||||
if modrinth_version
|
if modrinth_version
|
||||||
.original_sha1
|
.original_sha1
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|x| x == &version.sha1)
|
.is_some_and(|x| x == &version.sha1)
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
{
|
||||||
existing_versions.push(modrinth_version);
|
existing_versions.push(modrinth_version);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,59 +3,57 @@ use bytes::Bytes;
|
|||||||
use s3::creds::Credentials;
|
use s3::creds::Credentials;
|
||||||
use s3::{Bucket, Region};
|
use s3::{Bucket, Region};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, LazyLock};
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
static BUCKET: LazyLock<Bucket> = LazyLock::new(|| {
|
||||||
static ref BUCKET : Bucket = {
|
let region = dotenvy::var("S3_REGION").unwrap();
|
||||||
let region = dotenvy::var("S3_REGION").unwrap();
|
let b = Bucket::new(
|
||||||
let b = Bucket::new(
|
&dotenvy::var("S3_BUCKET_NAME").unwrap(),
|
||||||
&dotenvy::var("S3_BUCKET_NAME").unwrap(),
|
if &*region == "r2" {
|
||||||
if &*region == "r2" {
|
Region::R2 {
|
||||||
Region::R2 {
|
account_id: dotenvy::var("S3_URL").unwrap(),
|
||||||
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()
|
|
||||||
} else {
|
} 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! {
|
if region == "path-style" {
|
||||||
pub static ref REQWEST_CLIENT: reqwest::Client = {
|
*b.with_path_style()
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
} else {
|
||||||
if let Ok(header) = reqwest::header::HeaderValue::from_str(&format!(
|
*b
|
||||||
"modrinth/daedalus/{} (support@modrinth.com)",
|
}
|
||||||
env!("CARGO_PKG_VERSION")
|
});
|
||||||
)) {
|
|
||||||
headers.insert(reqwest::header::USER_AGENT, header);
|
|
||||||
}
|
|
||||||
|
|
||||||
reqwest::Client::builder()
|
pub static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
|
||||||
.tcp_keepalive(Some(std::time::Duration::from_secs(10)))
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
.timeout(std::time::Duration::from_secs(15))
|
if let Ok(header) = reqwest::header::HeaderValue::from_str(&format!(
|
||||||
.default_headers(headers)
|
"modrinth/daedalus/{} (support@modrinth.com)",
|
||||||
.build()
|
env!("CARGO_PKG_VERSION")
|
||||||
.unwrap()
|
)) {
|
||||||
};
|
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))]
|
#[tracing::instrument(skip(bytes, semaphore))]
|
||||||
pub async fn upload_file_to_bucket(
|
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
|
BUCKET.put_object(key.clone(), &bytes).await
|
||||||
}
|
}
|
||||||
.map_err(|err| ErrorKind::S3 {
|
.map_err(|err| ErrorKind::S3 {
|
||||||
inner: err,
|
inner: Box::new(err),
|
||||||
file: path.clone(),
|
file: path.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,7 +201,7 @@ pub async fn download_file(
|
|||||||
inner: err,
|
inner: err,
|
||||||
item: url.to_string(),
|
item: url.to_string(),
|
||||||
}
|
}
|
||||||
.into())
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro check && astro build",
|
"lint": "astro check",
|
||||||
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
@@ -18,4 +19,4 @@
|
|||||||
"starlight-openapi": "^0.14.0",
|
"starlight-openapi": "^0.14.0",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||