You've already forked AstralRinth
forked from didirus/AstralRinth
Compare commits
190 Commits
AR-0.10.3
...
AR-0.10.60
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a92adfb82 | |||
| af4c627a04 | |||
| 1e725e6d03 | |||
|
|
1454e3351e | ||
|
|
6f59f4c110 | ||
|
|
8e0732bf01 | ||
|
|
0cf3c1a88e | ||
|
|
8a3171d7c4 | ||
|
|
e25d726da4 | ||
|
|
11e99cb9d3 | ||
|
|
632b09ff3f | ||
|
|
713571d50e | ||
|
|
4ad6daa45c | ||
|
|
9b5f172170 | ||
|
|
4f789a0ebc | ||
|
|
ee3ac37967 | ||
|
|
2aabcf36ee | ||
|
|
82697278dc | ||
|
|
0bc6502443 | ||
|
|
5ffcc48d75 | ||
|
|
b81e727204 | ||
|
|
9ea43a12fd | ||
|
|
b279c43069 | ||
|
|
9497ba70a4 | ||
|
|
c02b809601 | ||
| 1d000bb238 | |||
|
|
df1499047c | ||
|
|
80eb297284 | ||
|
|
58645b9ba9 | ||
|
|
544f63512a | ||
|
|
3b8cd661bc | ||
|
|
8af65f58d9 | ||
|
|
ab79e84398 | ||
|
|
cf190d86d5 | ||
|
|
ca0c16b1fe | ||
|
|
17c9e4a721 | ||
|
|
d7f1029b54 | ||
|
|
ad208536b0 | ||
| 553db55c7b | |||
|
|
d22c9e24f4 | ||
|
|
e31197f649 | ||
|
|
0dee21814d | ||
|
|
0657e4466f | ||
|
|
13dbb4c57e | ||
| 4c6290ead6 | |||
|
|
99493b9917 | ||
|
|
72a52eb7b1 | ||
|
|
b33e12c71d | ||
|
|
82d86839c7 | ||
|
|
3a20e15340 | ||
|
|
1c89b84314 | ||
| 8d36c14554 | |||
|
|
6387fb21c6 | ||
|
|
c7d0839bfb | ||
| 2b43e26a85 | |||
|
|
175b90be5a | ||
|
|
13103b4950 | ||
|
|
8804478221 | ||
|
|
b8982a6d17 | ||
|
|
ff88724d01 | ||
|
|
7dffb352d5 | ||
|
|
1df6e29aa1 | ||
|
|
5deb4179ad | ||
|
|
358cf31c87 | ||
| 7cea4b21a8 | |||
| 7846fd00aa | |||
| cebc195fe0 | |||
|
|
6db1d66591 | ||
|
|
8052fda840 | ||
| ae58f3844d | |||
| acd4b1696a | |||
| 5ea78b78c2 | |||
| f90998157d | |||
| 634000cdb6 | |||
|
|
5fd8c38c1c | ||
|
|
15892a88d3 | ||
|
|
32793c50e1 | ||
|
|
0e0ca1971a | ||
|
|
bb9af18eed | ||
|
|
d4516d3527 | ||
|
|
87de47fe5e | ||
|
|
7d76fe1b6a | ||
| 46d30e491a | |||
| 059c0618f1 | |||
| 7ef60fcafe | |||
| ec17e79014 | |||
| e351d674f4 | |||
| f555fa916a | |||
| dbe38cb4e7 | |||
| 2e40e26116 | |||
|
|
ae25a15abd | ||
|
|
0f755b94ce | ||
|
|
bcf46d440b | ||
|
|
526561f2de | ||
|
|
a8caa1afc3 | ||
|
|
98e9a8473d | ||
|
|
936395484e | ||
|
|
0c3e23db96 | ||
|
|
013ba4d86d | ||
|
|
93813c448c | ||
|
|
c20b869e62 | ||
|
|
56c556821b | ||
|
|
44267619b6 | ||
| 10afd673db | |||
|
|
90043fe84d | ||
|
|
a6a98ff63e | ||
|
|
911652133b | ||
|
|
cee1b5f522 | ||
|
|
62f5a23fcb | ||
| 5a10292add | |||
| 3f606a08aa | |||
| 2d5d747202 | |||
|
|
eb595cdc3e | ||
| 7516ff9e47 | |||
|
|
572cd065ed | ||
| df9bbe3ba0 | |||
| 362fd7f32a | |||
|
|
76dc8a0897 | ||
|
|
4723de6269 | ||
|
|
e15fa35bad | ||
| 7716a0c524 | |||
|
|
2cc6bc8ce4 | ||
|
|
5d19d31b2c | ||
|
|
c1b95ede07 | ||
|
|
058185c7fd | ||
|
|
6fb125cf0f | ||
|
|
a945e9b005 | ||
|
|
b943638afb | ||
|
|
207dc0e2bb | ||
|
|
359fbd4738 | ||
| adf831dab9 | |||
| efeac22d14 | |||
| 591d98a9eb | |||
| 77472d9a09 | |||
| 789d666515 | |||
| d917bff6ef | |||
| 4e69cd8bde | |||
| b71e4cc6f9 | |||
| a56ab6adb9 | |||
| f1b67c9584 | |||
| 3d32640b83 | |||
| 6dfb599e14 | |||
| 332a543f66 | |||
| 1ef96c447e | |||
| 1ec92b5f97 | |||
| f0a4532051 | |||
|
|
f7700acce4 | ||
|
|
87a3e2d022 | ||
|
|
5d17663040 | ||
|
|
cff3c72f94 | ||
|
|
fadf475f06 | ||
|
|
7228499737 | ||
|
|
bca467a634 | ||
| 14f6450cf4 | |||
| 14bf06e4bd | |||
|
|
cb72d2ac80 | ||
|
|
3c79607d1f | ||
| 97bd18c7b3 | |||
| 8af0288274 | |||
| 167072de0c | |||
| 2df37be9a7 | |||
| 34d85a03b2 | |||
| 17cf5e3132 | |||
|
|
36ad1f16e4 | ||
|
|
5d4f334505 | ||
|
|
1fdb5ba748 | ||
| c5e67a5c6f | |||
| e2e21c1496 | |||
| 6da942ccbb | |||
|
|
26df6f51ef | ||
|
|
6caf794ae1 | ||
|
|
2692953e31 | ||
|
|
242fd713ab | ||
|
|
7a12c4d5e2 | ||
| 0ab4dec62d | |||
|
|
f256ef43c0 | ||
| 3ecb20afd6 | |||
| 1e10f24efe | |||
| 006fd7c7f5 | |||
| 1e8e001eb8 | |||
| 585935c799 | |||
|
|
e0cde2d6ff | ||
| a64c3360d2 | |||
|
|
e4e77dc0d2 | ||
|
|
8ba6467f21 | ||
| a2b2711204 | |||
|
|
088cb54317 | ||
| ab57926e44 | |||
| 35cd79727a | |||
|
|
c47bcf665d |
@@ -2,5 +2,8 @@
|
|||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
||||||
|
|
||||||
|
[target.x86_64-pc-windows-msvc]
|
||||||
|
linker = "rust-lld"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
|
|||||||
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
.gitignore
|
||||||
@@ -3,16 +3,23 @@ root = true
|
|||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = tab
|
||||||
indent_size = 2
|
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
max_line_length = 100
|
max_line_length = 100
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
|
indent_size = 2
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.{rs,java,kts}]
|
[*.toml]
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# YAML requires space indentation by spec
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
|||||||
56
.github/workflows/astralrinth-build.yml
vendored
56
.github/workflows/astralrinth-build.yml
vendored
@@ -16,6 +16,7 @@ on:
|
|||||||
- 'packages/assets/**'
|
- 'packages/assets/**'
|
||||||
- 'packages/ui/**'
|
- 'packages/ui/**'
|
||||||
- 'packages/utils/**'
|
- 'packages/utils/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -23,10 +24,11 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [macos-latest, windows-latest, ubuntu-latest]
|
# platform: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
platform: [windows-latest, ubuntu-latest]
|
||||||
include:
|
include:
|
||||||
- platform: macos-latest
|
# - platform: macos-latest
|
||||||
artifact-target-name: universal-apple-darwin
|
# artifact-target-name: universal-apple-darwin
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
artifact-target-name: x86_64-pc-windows-msvc
|
artifact-target-name: x86_64-pc-windows-msvc
|
||||||
- platform: ubuntu-latest
|
- platform: ubuntu-latest
|
||||||
@@ -40,6 +42,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: 🔍 Validate Git config does not introduce CRLF
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Checking Git config for CRLF settings..."
|
||||||
|
|
||||||
|
autocrlf=$(git config --get core.autocrlf || echo "unset")
|
||||||
|
eol_setting=$(git config --get core.eol || echo "unset")
|
||||||
|
|
||||||
|
echo "core.autocrlf = $autocrlf"
|
||||||
|
echo "core.eol = $eol_setting"
|
||||||
|
|
||||||
|
if [ "$autocrlf" = "true" ]; then
|
||||||
|
echo "⚠️ WARNING: core.autocrlf is set to 'true'. Consider setting it to 'input' or 'false'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$eol_setting" = "crlf" ]; then
|
||||||
|
echo "⚠️ WARNING: core.eol is set to 'crlf'. Consider unsetting it or setting to 'lf'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 🔍 Check migration files line endings (LF only)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Scanning migration SQL files for CR characters (\\r)..."
|
||||||
|
if grep -Iq $'\r' packages/app-lib/migrations/*.sql; then
|
||||||
|
echo "❌ ERROR: Some migration files contain CR (\\r) characters — expected only LF line endings."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ All migration files use LF line endings"
|
||||||
|
|
||||||
- name: 🧰 Setup Rust toolchain
|
- name: 🧰 Setup Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -66,6 +97,11 @@ jobs:
|
|||||||
xdg-utils \
|
xdg-utils \
|
||||||
openjdk-11-jdk
|
openjdk-11-jdk
|
||||||
|
|
||||||
|
- name: ⚙️ Set application environment
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp packages/app-lib/.env.prod packages/app-lib/.env
|
||||||
|
|
||||||
- name: 💨 Setup Turbo cache
|
- name: 💨 Setup Turbo cache
|
||||||
uses: rharkor/caching-for-turbo@v1.8
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
|
|
||||||
@@ -73,7 +109,7 @@ jobs:
|
|||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: ✍️ Set up Windows code signing (jsign)
|
- name: ✍️ Set up Windows code signing (jsign)
|
||||||
if: matrix.platform == 'windows' && env.SIGN_WINDOWS_BINARIES == 'true'
|
if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
choco install jsign --ignore-dependencies
|
choco install jsign --ignore-dependencies
|
||||||
@@ -84,12 +120,12 @@ jobs:
|
|||||||
rm -rf target/release/bundle
|
rm -rf target/release/bundle
|
||||||
rm -rf target/*/release/bundle || true
|
rm -rf target/*/release/bundle || true
|
||||||
|
|
||||||
- name: 🔨 Build macOS app
|
# - name: 🔨 Build macOS app
|
||||||
if: matrix.platform == 'macos-latest'
|
# if: matrix.platform == 'macos-latest'
|
||||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
# run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||||
env:
|
# env:
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: 🔨 Build Linux app
|
- name: 🔨 Build Linux app
|
||||||
if: matrix.platform == 'ubuntu-latest'
|
if: matrix.platform == 'ubuntu-latest'
|
||||||
|
|||||||
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
20
.idea/code.iml
generated
20
.idea/code.iml
generated
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/daedalus_client/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/daedalus/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/app-playground/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/app/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/rust-common/src" isTestSource="false" />
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
7
.idea/discord.xml
generated
7
.idea/discord.xml
generated
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DiscordProjectSettings">
|
|
||||||
<option name="show" value="PROJECT_FILES" />
|
|
||||||
<option name="description" value="" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
26
.idea/libraries/KotlinJavaRuntime.xml
generated
26
.idea/libraries/KotlinJavaRuntime.xml
generated
@@ -1,26 +0,0 @@
|
|||||||
<component name="libraryTable">
|
|
||||||
<library name="KotlinJavaRuntime" type="repository">
|
|
||||||
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0" />
|
|
||||||
<CLASSES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0.jar!/" />
|
|
||||||
</CLASSES>
|
|
||||||
<JAVADOC>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-javadoc.jar!/" />
|
|
||||||
</JAVADOC>
|
|
||||||
<SOURCES>
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.0/kotlin-stdlib-jdk8-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.8.0/kotlin-stdlib-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.8.0/kotlin-stdlib-common-1.8.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
|
|
||||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.0/kotlin-stdlib-jdk7-1.8.0-sources.jar!/" />
|
|
||||||
</SOURCES>
|
|
||||||
</library>
|
|
||||||
</component>
|
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/code.iml" filepath="$PROJECT_DIR$/.idea/code.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
.idea/vcs.xml
generated
12
.idea/vcs.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CommitMessageInspectionProfile">
|
|
||||||
<profile version="1.0">
|
|
||||||
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Cargo.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.github/**/*.png
|
||||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -2,8 +2,14 @@
|
|||||||
"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.detectIndentation": false,
|
||||||
|
"editor.insertSpaces": false,
|
||||||
|
"files.eol": "\n",
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit",
|
||||||
}
|
"source.organizeImports": "always"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
All packages in this repository are licensed under their respective licenses. For more information, refer to the LICENSE file in each package.
|
All packages in this repository are licensed under their respective licenses. For more information, refer to the LICENSE file in each package.
|
||||||
|
|
||||||
For detailed information, consult each package's COPYING.md file, if available.
|
For detailed information, consult each package's COPYING.md, LICENSE.txt, or LICENSE file, if available.
|
||||||
|
|
||||||
## Modrinth Branding
|
## Modrinth Branding
|
||||||
|
|
||||||
|
|||||||
1545
Cargo.lock
generated
1545
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
88
Cargo.toml
88
Cargo.toml
@@ -25,31 +25,29 @@ actix-ws = "0.3.0"
|
|||||||
argon2 = { version = "0.5.3", features = ["std"] }
|
argon2 = { version = "0.5.3", features = ["std"] }
|
||||||
ariadne = { path = "packages/ariadne" }
|
ariadne = { path = "packages/ariadne" }
|
||||||
async_zip = "0.0.17"
|
async_zip = "0.0.17"
|
||||||
async-compression = { version = "0.4.25", default-features = false }
|
async-compression = { version = "0.4.27", default-features = false }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-stripe = { version = "0.41.0", default-features = false, features = [
|
async-stripe = { version = "0.41.0", default-features = false, features = [
|
||||||
"runtime-tokio-hyper-rustls",
|
"runtime-tokio-hyper-rustls",
|
||||||
] }
|
] }
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
async-tungstenite = { version = "0.29.1", default-features = false, features = [
|
async-tungstenite = { version = "0.30.0", default-features = false, features = ["futures-03-sink"] }
|
||||||
"futures-03-sink",
|
|
||||||
] }
|
|
||||||
async-walkdir = "2.1.0"
|
async-walkdir = "2.1.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bitflags = "2.9.1"
|
bitflags = "2.9.1"
|
||||||
bytemuck = "1.23.0"
|
bytemuck = "1.23.1"
|
||||||
bytes = "1.10.1"
|
bytes = "1.10.1"
|
||||||
censor = "0.3.0"
|
censor = "0.3.0"
|
||||||
chardetng = "0.1.17"
|
chardetng = "0.1.17"
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
clap = "4.5.40"
|
clap = "4.5.43"
|
||||||
clickhouse = "0.13.3"
|
clickhouse = "0.13.3"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
console-subscriber = "0.4.1"
|
console-subscriber = "0.4.1"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
data-url = "0.3.1"
|
data-url = "0.3.1"
|
||||||
deadpool-redis = "0.21.1"
|
deadpool-redis = "0.22.0"
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
discord-rich-presence = "0.2.5"
|
discord-rich-presence = "0.2.5"
|
||||||
dotenv-build = "0.1.1"
|
dotenv-build = "0.1.1"
|
||||||
@@ -57,7 +55,7 @@ dotenvy = "0.15.7"
|
|||||||
dunce = "1.0.5"
|
dunce = "1.0.5"
|
||||||
either = "1.15.0"
|
either = "1.15.0"
|
||||||
encoding_rs = "0.8.35"
|
encoding_rs = "0.8.35"
|
||||||
enumset = "1.1.6"
|
enumset = "1.1.7"
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.2"
|
||||||
fs4 = { version = "0.13.1", default-features = false }
|
fs4 = { version = "0.13.1", default-features = false }
|
||||||
futures = { version = "0.3.31", default-features = false }
|
futures = { version = "0.3.31", default-features = false }
|
||||||
@@ -67,16 +65,22 @@ heck = "0.5.0"
|
|||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper-tls = "0.6.0"
|
hyper = "1.6.0"
|
||||||
hyper-util = "0.1.14"
|
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||||
|
"http1",
|
||||||
|
"native-tokio",
|
||||||
|
"ring",
|
||||||
|
"tls12",
|
||||||
|
] }
|
||||||
|
hyper-util = "0.1.16"
|
||||||
iana-time-zone = "0.1.63"
|
iana-time-zone = "0.1.63"
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||||
indexmap = "2.9.0"
|
indexmap = "2.10.0"
|
||||||
indicatif = "0.17.11"
|
indicatif = "0.18.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jemalloc_pprof = "0.7.0"
|
jemalloc_pprof = "0.8.1"
|
||||||
json-patch = { version = "4.0.0", default-features = false }
|
json-patch = { version = "4.0.0", default-features = false }
|
||||||
lettre = { version = "0.11.17", default-features = false, features = [
|
lettre = { version = "0.11.18", default-features = false, features = [
|
||||||
"builder",
|
"builder",
|
||||||
"hostname",
|
"hostname",
|
||||||
"pool",
|
"pool",
|
||||||
@@ -86,27 +90,25 @@ lettre = { version = "0.11.17", default-features = false, features = [
|
|||||||
"smtp-transport",
|
"smtp-transport",
|
||||||
] }
|
] }
|
||||||
maxminddb = "0.26.0"
|
maxminddb = "0.26.0"
|
||||||
meilisearch-sdk = { version = "0.28.0", default-features = false }
|
meilisearch-sdk = { version = "0.29.1", default-features = false }
|
||||||
murmur2 = "0.1.0"
|
murmur2 = "0.1.0"
|
||||||
native-dialog = "0.9.0"
|
native-dialog = "0.9.0"
|
||||||
notify = { version = "8.0.0", default-features = false }
|
notify = { version = "8.2.0", default-features = false }
|
||||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
png = "0.17.16"
|
png = "0.17.16"
|
||||||
prometheus = "0.14.0"
|
prometheus = "0.14.0"
|
||||||
quartz_nbt = "0.2.9"
|
quartz_nbt = "0.2.9"
|
||||||
quick-xml = "0.37.5"
|
quick-xml = "0.38.1"
|
||||||
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
|
||||||
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
|
||||||
redis = "=0.31.0" # Locked on 0.31 until deadpool-redis updates to 0.32
|
redis = "0.32.4"
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
reqwest = { version = "0.12.20", default-features = false }
|
reqwest = { version = "0.12.22", default-features = false }
|
||||||
rgb = "0.8.50"
|
rgb = "0.8.52"
|
||||||
rust_decimal = { version = "1.37.2", features = [
|
rust_decimal = { version = "1.37.2", features = ["serde-with-float", "serde-with-str"] }
|
||||||
"serde-with-float",
|
|
||||||
"serde-with-str",
|
|
||||||
] }
|
|
||||||
rust_iso3166 = "0.1.14"
|
rust_iso3166 = "0.1.14"
|
||||||
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
rust-s3 = { version = "0.35.1", default-features = false, features = [
|
||||||
"fail-on-err",
|
"fail-on-err",
|
||||||
@@ -114,7 +116,7 @@ rust-s3 = { version = "0.35.1", default-features = false, features = [
|
|||||||
"tokio-rustls-tls",
|
"tokio-rustls-tls",
|
||||||
] }
|
] }
|
||||||
rusty-money = "0.4.1"
|
rusty-money = "0.4.1"
|
||||||
sentry = { version = "0.41.0", default-features = false, features = [
|
sentry = { version = "0.42.0", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"contexts",
|
"contexts",
|
||||||
"debug-images",
|
"debug-images",
|
||||||
@@ -122,45 +124,45 @@ sentry = { version = "0.41.0", default-features = false, features = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
] }
|
] }
|
||||||
sentry-actix = "0.41.0"
|
sentry-actix = "0.42.0"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
serde_bytes = "0.11.17"
|
serde_bytes = "0.11.17"
|
||||||
serde_cbor = "0.11.2"
|
serde_cbor = "0.11.2"
|
||||||
serde_ini = "0.2.0"
|
serde_ini = "0.2.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.142"
|
||||||
serde_with = "3.13.0"
|
serde_with = "3.14.0"
|
||||||
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
serde-xml-rs = "0.8.1" # Also an XML (de)serializer, consider dropping yaserde in favor of this
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
sha1_smol = { version = "1.0.1", features = ["std"] }
|
sha1_smol = { version = "1.0.1", features = ["std"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
spdx = "0.10.8"
|
spdx = "0.10.9"
|
||||||
sqlx = { version = "0.8.6", default-features = false }
|
sqlx = { version = "0.8.6", default-features = false }
|
||||||
sysinfo = { version = "0.35.2", default-features = false }
|
sysinfo = { version = "0.36.1", default-features = false }
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tauri = "2.6.1"
|
tauri = "2.7.0"
|
||||||
tauri-build = "2.3.0"
|
tauri-build = "2.3.1"
|
||||||
tauri-plugin-deep-link = "2.4.0"
|
tauri-plugin-deep-link = "2.4.1"
|
||||||
tauri-plugin-dialog = "2.3.0"
|
tauri-plugin-dialog = "2.3.2"
|
||||||
tauri-plugin-http = "2.5.0"
|
tauri-plugin-http = "2.5.1"
|
||||||
tauri-plugin-opener = "2.4.0"
|
tauri-plugin-opener = "2.4.0"
|
||||||
tauri-plugin-os = "2.3.0"
|
tauri-plugin-os = "2.3.0"
|
||||||
tauri-plugin-single-instance = "2.3.0"
|
tauri-plugin-single-instance = "2.3.2"
|
||||||
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
tauri-plugin-updater = { version = "2.9.0", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"zip",
|
"zip",
|
||||||
] }
|
] }
|
||||||
tauri-plugin-window-state = "2.3.0"
|
tauri-plugin-window-state = "2.4.0"
|
||||||
tempfile = "3.20.0"
|
tempfile = "3.20.0"
|
||||||
theseus = { path = "packages/app-lib" }
|
theseus = { path = "packages/app-lib" }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tikv-jemalloc-ctl = "0.6.0"
|
tikv-jemalloc-ctl = "0.6.0"
|
||||||
tikv-jemallocator = "0.6.0"
|
tikv-jemallocator = "0.6.0"
|
||||||
tokio = "1.45.1"
|
tokio = "1.47.1"
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.16"
|
||||||
totp-rs = "5.7.0"
|
totp-rs = "5.7.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-actix-web = "0.7.18"
|
tracing-actix-web = "0.7.19"
|
||||||
tracing-error = "0.2.1"
|
tracing-error = "0.2.1"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = "0.3.19"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
@@ -172,7 +174,7 @@ whoami = "1.6.0"
|
|||||||
winreg = "0.55.0"
|
winreg = "0.55.0"
|
||||||
woothee = "0.13.0"
|
woothee = "0.13.0"
|
||||||
yaserde = "0.12.0"
|
yaserde = "0.12.0"
|
||||||
zip = { version = "4.2.0", default-features = false, features = [
|
zip = { version = "4.3.0", default-features = false, features = [
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"deflate",
|
"deflate",
|
||||||
"deflate64",
|
"deflate64",
|
||||||
@@ -219,7 +221,7 @@ wildcard_dependencies = "warn"
|
|||||||
warnings = "deny"
|
warnings = "deny"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
wry = { git = "https://github.com/modrinth/wry", rev = "21db186" }
|
wry = { git = "https://github.com/modrinth/wry", rev = "f2ce0b0" }
|
||||||
|
|
||||||
# Optimize for speed and reduce size on release builds
|
# Optimize for speed and reduce size on release builds
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
157
README.md
157
README.md
@@ -1,76 +1,123 @@
|
|||||||
# Navigation in this README
|
# 📘 Navigation
|
||||||
- [Install instructions](#install-instructions)
|
|
||||||
- [Features](#features)
|
- [🔧 Install Instructions](#install-instructions)
|
||||||
- [Getting started](#getting-started)
|
- [✨ Features](#features)
|
||||||
- [Disclaimer](#disclaimer)
|
- [🚀 Getting Started](#getting-started)
|
||||||
- [Donate](#support-our-project-crypto-wallets)
|
- [⚠️ Disclaimer](#disclaimer)
|
||||||
|
- [💰 Donate](#support-our-project-crypto-wallets)
|
||||||
|
|
||||||
|
## Other languages
|
||||||
|
> [Русский](readme/ru_ru/README.md)
|
||||||
|
|
||||||
|
## Support channel
|
||||||
|
> [Telegram](https://me.astralium.su/ref/telegram_channel)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# About Project
|
# About Project
|
||||||
|
|
||||||
## AstralRinth • Empowering Your Minecraft Adventure
|
## **AstralRinth • Empowering Your Minecraft Adventure**
|
||||||
Welcome to AR • Fork of Modrinth, the ultimate game launcher designed to enhance your Minecraft experience through the Modrinth platform and their API. Whether you're a graphical interface enthusiast, or a developer integrating Modrinth projects, Theseus core is your gateway to a new level of Minecraft gaming.
|
|
||||||
|
|
||||||
## About Software
|
Welcome to **AstralRinth (AR)** — a powerful fork of Modrinth, reimagined to enhance your Minecraft journey. Whether you're a GUI enthusiast or a developer building with Modrinth’s API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
|
||||||
Introducing AstralRinth, a specialized variant of Theseus dedicated to implementing offline authorization for an even more flexible and user-centric Minecraft Modrinth experience. Roam the Minecraft realms without the constraints of online authentication, thanks to AstralRinth.
|
|
||||||
|
|
||||||
## AR • Unlocking Minecraft's Boundless Horizon
|
- *Recently, improved integration with the Git Astralium API has been added.*
|
||||||
Dive into the extraordinary world of AstralRinth, a fork of the original project with a unique focus on providing a free trial experience for Minecraft, all without the need for a license. Currently boasting:
|
|
||||||
|
|
||||||
# Install instructions
|
## **About the Software**
|
||||||
- To install our application, you need to download a file for your operating system from our available releases or development builds • [Download variants here](https://git.astralium.su/didirus/AstralRinth/releases)
|
|
||||||
- After you have downloaded the required executable file or archive, then open it
|
|
||||||
|
|
||||||
### Downloadable file extensions
|
**AstralRinth** is a dedicated branch of the Theseus project, focused on **offline authentication**, offering you more flexibility and control. Play Minecraft without the need for constant online verification — a user-first approach to modern modded gaming.
|
||||||
- `.msi` format for Windows OS system _(Supported popular latest versions of Microsoft Windows)_
|
|
||||||
- `.dmg` format for MacOS system _(Works on Macos Ventura / Sonoma / Sequoia, but it should be works on older OS builds)_
|
|
||||||
- `.deb` format for Linux OS systems _(Since there are quite a few distributions, we do not guarantee
|
|
||||||
|
|
||||||
### Installation subjects
|
## **AR • Unlocking Minecraft's Boundless Horizon**
|
||||||
- Builds in releases that are signed with the following prefixes are not recommended for installation and may contain errors:
|
|
||||||
- `dev`
|
This unique fork introduces a **free trial Minecraft experience**, bypassing license checks while maintaining rich functionality. Currently includes:
|
||||||
- `nightly`
|
|
||||||
- `dirty`
|
---
|
||||||
- `dirty-dev`
|
|
||||||
- `dirty-nightly`
|
# Install Instructions
|
||||||
- `dirty_dev`
|
|
||||||
- `dirty_nightly`
|
To install the launcher:
|
||||||
- Auto-updating takes place through parsing special versions from releases, so we also distribute clean types of `.msi, .dmg and .deb`
|
|
||||||
|
1. Visit the [releases page](https://git.astralium.su/didirus/AstralRinth/releases) to download the correct version for your system.
|
||||||
|
2. Run the downloaded file or extract and launch it, depending on the format.
|
||||||
|
|
||||||
|
### Downloadable File Extensions
|
||||||
|
|
||||||
|
| Extension | OS | Notes |
|
||||||
|
| --------- | ------- | --------------------------------------------------------------------- |
|
||||||
|
| `.msi` | Windows | Supported on all recent Windows versions |
|
||||||
|
| `.dmg` | macOS | Works on Ventura, Sonoma, Sequoia _(may also support older versions)_ |
|
||||||
|
| `.deb` | Linux | Basic support; compatibility may vary by distribution |
|
||||||
|
|
||||||
|
### Installation Warnings
|
||||||
|
|
||||||
|
Avoid using builds with these prefixes — they may be unstable or experimental:
|
||||||
|
|
||||||
|
- `dev`
|
||||||
|
- `nightly`
|
||||||
|
- `dirty`
|
||||||
|
- `dirty-dev`
|
||||||
|
- `dirty-nightly`
|
||||||
|
- `dirty_dev`
|
||||||
|
- `dirty_nightly`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
### Featured enhancement in AR
|
> _The launcher provides an opportunity to use the well-known Modrinth, but with an improved user experience._
|
||||||
- AstralRinth offers a range of authorization options, giving users the flexibility to log in with valid licenses or even a pirate account without auth credentials breaks (_Unlike MultiMC Cracked and similar software_). Experience Minecraft on your terms, breaking free from traditional licensing constraints (_Popular in Russian Federation_).
|
|
||||||
|
|
||||||
### Easy to use
|
## Included exclusive features
|
||||||
- Using the launcher is intuitive, any user can figure it out.
|
|
||||||
|
|
||||||
### Update notifies
|
- No ads in the entire launcher.
|
||||||
- We have implemented notifications about the release of new updates on our Git. The launcher can also download them for you and try to install them.
|
- Custom `.svg` vector icons for a distinct UI.
|
||||||
|
- Improved compatibility with both licensed and pirate accounts.
|
||||||
|
- Use **official microsoft accounts** or **offline/pirate accounts** — login won't break.
|
||||||
|
- Supports license-free access for testing or personal use.
|
||||||
|
- No dependence on official authentication services.
|
||||||
|
- Discord Rich Presence integration:
|
||||||
|
- Dynamic status messages.
|
||||||
|
- In-game timer and AFK counter.
|
||||||
|
- Strict disabling of statistics and other Modrinth metrics.
|
||||||
|
- Optimized archive/package size.
|
||||||
|
- Integrated update fetcher for seamless version management.
|
||||||
|
- Built-in update alerts for new versions posted on Git Astralium.
|
||||||
|
- Automatic download and installation capabilities.
|
||||||
|
- Database migration fixes, when error occurred (Interactive Mode) (Modrinth issue)
|
||||||
|
- ElyBy skin system integration (AuthLib / Java)
|
||||||
|
|
||||||
### Enhancements
|
---
|
||||||
- Custom .SVG vectors for a personalized touch.
|
|
||||||
- Improved compatibility for both pirate and licensed accounts.
|
|
||||||
- Beautiful Discord RPC with random messages while playing, along with an in-game timer and AFK counter.
|
|
||||||
- Forced disabling of statistics collection (modrinch metrics) with a hard patch from AstralRinth, ensuring it remains deactivated regardless of the configuration setting.
|
|
||||||
- Removal of advertisements from all launcher views.
|
|
||||||
- Optimization of packages (archives).
|
|
||||||
- Integrated update fetching feature
|
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
To begin your AstralRinth adventure, follow these steps:
|
|
||||||
1. **Download Your OS Version**: Head over to our [releases page](https://git.astralium.su/didirus/AstralRinth/releases/) to find the right file for your operating system.
|
To begin using AstralRinth:
|
||||||
- **Choosing the Correct File**: Ensure you select the file that matches your OS requirements.
|
|
||||||
- [**How select file**](#downloadable-file-extensions)
|
1. **Download Your OS Version**
|
||||||
- [**How select release**](#installation-subjects)
|
|
||||||
2. **Authentication**: Log in with a valid license or, for testing, try using a pirate account to see AstralRinth in action.
|
- Go to the [releases page](https://git.astralium.su/didirus/AstralRinth/releases)
|
||||||
3. **Launch Minecraft**: Start your journey by launching Minecraft through AstralRinth and enjoy the adventures that await.
|
- [How to choose a file](#downloadable-file-extensions)
|
||||||
- **Choosing java installation**: The launcher will try to automatically detect the recommended JVM version for running the game, but you can configure everything in the launcher settings.
|
- [How to choose a release](#installation-warnings)
|
||||||
|
|
||||||
|
2. **Log In**
|
||||||
|
|
||||||
|
- Use your official Mojang/Microsoft account, or test using a non-licensed account.
|
||||||
|
|
||||||
|
3. **Launch Minecraft**
|
||||||
|
- Start Minecraft from the launcher.
|
||||||
|
- The launcher will auto-detect the recommended JVM version.
|
||||||
|
- You can also configure Java manually in the settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
- AstralRinth is a project intended for experimentation and educational purposes only. It does not endorse or support piracy, and users are encouraged to obtain valid licenses for a fully-supported Minecraft experience.
|
|
||||||
- Users are reminded to respect licensing agreements and support the developers of Minecraft.
|
|
||||||
|
|
||||||
# Support our Project (Crypto Wallets)
|
- **AstralRinth** is intended **solely for educational and experimental use**.
|
||||||
|
- We **do not condone piracy** — users are encouraged to purchase a legitimate Minecraft license.
|
||||||
|
- Respect all relevant licensing agreements and support Minecraft developers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Support Our Project (Crypto Wallets)
|
||||||
|
|
||||||
|
If you'd like to support development, you can donate via the following crypto wallets:
|
||||||
|
|
||||||
- BTC (Telegram): 14g6asNYzcUoaQtB8B2QGKabgEvn55wfLj
|
- BTC (Telegram): 14g6asNYzcUoaQtB8B2QGKabgEvn55wfLj
|
||||||
- USDT TRC20 (Telegram): TMSmv1D5Fdf4fipUpwBCdh16WevrV45vGr
|
|
||||||
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe
|
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
**/dist
|
**/dist
|
||||||
*.gltf
|
*.gltf
|
||||||
|
src/locales/
|
||||||
|
src/assets/**/*.svg
|
||||||
|
|||||||
@@ -1,22 +1,2 @@
|
|||||||
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
|
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||||
import { fixupPluginRules } from '@eslint/compat'
|
export default config
|
||||||
import turboPlugin from 'eslint-plugin-turbo'
|
|
||||||
|
|
||||||
export default createConfigForNuxt().append([
|
|
||||||
{
|
|
||||||
name: 'turbo',
|
|
||||||
plugins: {
|
|
||||||
turbo: fixupPluginRules(turboPlugin),
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'turbo/no-undeclared-env-vars': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'modrinth',
|
|
||||||
rules: {
|
|
||||||
'vue/html-self-closing': 'off',
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script src="https://tally.so/widgets/embed.js" async></script>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"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/**/*.{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"
|
"test": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"vue-virtual-scroller": "v2.0.0-beta.8"
|
"vue-virtual-scroller": "v2.0.0-beta.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@modrinth/tooling-config": "workspace:*",
|
||||||
"@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",
|
||||||
@@ -48,13 +49,11 @@
|
|||||||
"@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-plugin-turbo": "^2.5.4",
|
"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",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"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"
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, ref, watch, provide } from 'vue'
|
|
||||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
|
||||||
import {
|
import {
|
||||||
ArrowBigUpDashIcon,
|
ArrowBigUpDashIcon,
|
||||||
ChangeSkinIcon,
|
ChangeSkinIcon,
|
||||||
@@ -13,21 +11,23 @@ import {
|
|||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
MaximizeIcon,
|
MaximizeIcon,
|
||||||
MinimizeIcon,
|
MinimizeIcon,
|
||||||
|
NewspaperIcon,
|
||||||
|
NotepadTextIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RestoreIcon,
|
RestoreIcon,
|
||||||
RightArrowIcon,
|
RightArrowIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
WorldIcon,
|
WorldIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
NewspaperIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
Notifications,
|
|
||||||
OverflowMenu,
|
|
||||||
NewsArticleCard,
|
NewsArticleCard,
|
||||||
|
NotificationPanel,
|
||||||
|
OverflowMenu,
|
||||||
|
provideNotificationManager,
|
||||||
} from '@modrinth/ui'
|
} 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'
|
||||||
@@ -39,42 +39,54 @@ import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
|||||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||||
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
||||||
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
||||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
|
||||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||||
import { type } from '@tauri-apps/plugin-os'
|
import { type } from '@tauri-apps/plugin-os'
|
||||||
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
|
|
||||||
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import { renderString } from '@modrinth/utils'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { create_profile_and_install_from_file } from './helpers/pack'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { useError } from '@/store/error.js'
|
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
import { $fetch } from 'ofetch'
|
||||||
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
|
||||||
|
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||||
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
|
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
|
||||||
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
||||||
import { useInstall } from '@/store/install.js'
|
import { useInstall } from '@/store/install.js'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
|
||||||
import { renderString } from '@modrinth/utils'
|
|
||||||
import { useFetch } from '@/helpers/fetch.js'
|
import { useFetch } from '@/helpers/fetch.js'
|
||||||
// import { check } from '@tauri-apps/plugin-updater'
|
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
||||||
import { get_user } from '@/helpers/cache.js'
|
import { get_user } from '@/helpers/cache.js'
|
||||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||||
// import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
||||||
// import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
||||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||||
|
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||||
|
import { list } from '@/helpers/profile.js'
|
||||||
|
import { getOS, isDev } from '@/helpers/utils.js'
|
||||||
|
import { useError } from '@/store/error.js'
|
||||||
|
|
||||||
|
import { create_profile_and_install_from_file } from './helpers/pack'
|
||||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||||
|
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||||
|
import { AppNotificationManager } from './providers/app-notifications'
|
||||||
|
|
||||||
|
// [AR] Feature
|
||||||
|
import { getRemote, updateState } from '@/helpers/update.js'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
|
const notificationManager = new AppNotificationManager()
|
||||||
|
provideNotificationManager(notificationManager)
|
||||||
|
const { handleError, addNotification } = notificationManager
|
||||||
|
|
||||||
const news = ref([])
|
const news = ref([])
|
||||||
|
const availableSurvey = ref(false)
|
||||||
|
|
||||||
const urlModal = ref(null)
|
const urlModal = ref(null)
|
||||||
|
|
||||||
@@ -99,6 +111,7 @@ const isMaximized = ref(false)
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await useCheckDisableMouseover()
|
await useCheckDisableMouseover()
|
||||||
|
await getRemote(false) // [AR] Check for updates
|
||||||
|
|
||||||
document.querySelector('body').addEventListener('click', handleClick)
|
document.querySelector('body').addEventListener('click', handleClick)
|
||||||
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
||||||
@@ -110,15 +123,14 @@ onUnmounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function setupApp() {
|
async function setupApp() {
|
||||||
stateInitialized.value = true
|
|
||||||
|
|
||||||
const settings = await get()
|
const settings = await get()
|
||||||
|
|
||||||
// Patched
|
// [AR] Patched
|
||||||
settings.personalized_ads = false
|
settings.personalized_ads = false
|
||||||
settings.telemetry = false
|
settings.telemetry = false
|
||||||
await set(settings)
|
await set(settings)
|
||||||
|
|
||||||
|
stateInitialized.value = true
|
||||||
const {
|
const {
|
||||||
native_decorations,
|
native_decorations,
|
||||||
theme,
|
theme,
|
||||||
@@ -131,8 +143,7 @@ async function setupApp() {
|
|||||||
toggle_sidebar,
|
toggle_sidebar,
|
||||||
developer_mode,
|
developer_mode,
|
||||||
feature_flags,
|
feature_flags,
|
||||||
} = settings
|
} = await get()
|
||||||
|
|
||||||
|
|
||||||
if (default_page === 'Library') {
|
if (default_page === 'Library') {
|
||||||
await router.push('/library')
|
await router.push('/library')
|
||||||
@@ -159,13 +170,13 @@ async function setupApp() {
|
|||||||
isMaximized.value = await getCurrentWindow().isMaximized()
|
isMaximized.value = await getCurrentWindow().isMaximized()
|
||||||
})
|
})
|
||||||
|
|
||||||
initAnalytics()
|
// initAnalytics()
|
||||||
if (!telemetry) {
|
if (!telemetry) {
|
||||||
console.info("[AR] Telemetry disabled by default (Hard patched).")
|
console.info("[AR] • Telemetry disabled by default (Hard patched).")
|
||||||
optOutAnalytics()
|
optOutAnalytics()
|
||||||
}
|
}
|
||||||
if (!personalized_ads) {
|
if (!personalized_ads) {
|
||||||
console.info("[AR] Personalized ads disabled by default (Hard patched).")
|
console.info("[AR] • Personalized ads disabled by default (Hard patched).")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dev) debugAnalytics()
|
if (dev) debugAnalytics()
|
||||||
@@ -181,14 +192,14 @@ async function setupApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await warning_listener((e) =>
|
await warning_listener((e) =>
|
||||||
notificationsWrapper.value.addNotification({
|
addNotification({
|
||||||
title: 'Warning',
|
title: 'Warning',
|
||||||
text: e.message,
|
text: e.message,
|
||||||
type: 'warn',
|
type: 'warn',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Patched by AstralRinth
|
/// [AR] Patch
|
||||||
// useFetch(
|
// useFetch(
|
||||||
// `https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
|
// `https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
|
||||||
// 'criticalAnnouncements',
|
// 'criticalAnnouncements',
|
||||||
@@ -235,6 +246,12 @@ async function setupApp() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to generate skin previews in app setup.', error)
|
console.warn('Failed to generate skin previews in app setup.', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (osType === 'windows') {
|
||||||
|
await processPendingSurveys()
|
||||||
|
} else {
|
||||||
|
console.info('Skipping user surveys on non-Windows platforms')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateFailed = ref(false)
|
const stateFailed = ref(false)
|
||||||
@@ -266,9 +283,6 @@ const route = useRoute()
|
|||||||
const loading = useLoading()
|
const loading = useLoading()
|
||||||
loading.setEnabled(false)
|
loading.setEnabled(false)
|
||||||
|
|
||||||
const notifications = useNotifications()
|
|
||||||
const notificationsWrapper = ref()
|
|
||||||
|
|
||||||
const error = useError()
|
const error = useError()
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
|
|
||||||
@@ -279,6 +293,8 @@ const incompatibilityWarningModal = ref()
|
|||||||
|
|
||||||
const credentials = ref()
|
const credentials = ref()
|
||||||
|
|
||||||
|
const modrinthLoginFlowWaitModal = ref()
|
||||||
|
|
||||||
async function fetchCredentials() {
|
async function fetchCredentials() {
|
||||||
const creds = await getCreds().catch(handleError)
|
const creds = await getCreds().catch(handleError)
|
||||||
if (creds && creds.user_id) {
|
if (creds && creds.user_id) {
|
||||||
@@ -288,8 +304,24 @@ async function fetchCredentials() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function signIn() {
|
async function signIn() {
|
||||||
await login().catch(handleError)
|
modrinthLoginFlowWaitModal.value.show()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login()
|
||||||
await fetchCredentials()
|
await fetchCredentials()
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
typeof error === 'object' &&
|
||||||
|
typeof error['message'] === 'string' &&
|
||||||
|
error.message.includes('Login canceled')
|
||||||
|
) {
|
||||||
|
// Not really an error due to being a result of user interaction, show nothing
|
||||||
|
} else {
|
||||||
|
handleError(error)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
modrinthLoginFlowWaitModal.value.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logOut() {
|
async function logOut() {
|
||||||
@@ -332,8 +364,6 @@ const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
invoke('show_window')
|
invoke('show_window')
|
||||||
|
|
||||||
notifications.setNotifs(notificationsWrapper.value)
|
|
||||||
|
|
||||||
error.setErrorModal(errorModal.value)
|
error.setErrorModal(errorModal.value)
|
||||||
|
|
||||||
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
||||||
@@ -409,6 +439,116 @@ function handleAuxClick(e) {
|
|||||||
e.target.dispatchEvent(event)
|
e.target.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupOldSurveyDisplayData() {
|
||||||
|
const threeWeeksAgo = new Date()
|
||||||
|
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21)
|
||||||
|
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i)
|
||||||
|
|
||||||
|
if (key.startsWith('survey-') && key.endsWith('-display')) {
|
||||||
|
const dateValue = new Date(localStorage.getItem(key))
|
||||||
|
if (dateValue < threeWeeksAgo) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSurvey() {
|
||||||
|
if (!availableSurvey.value) {
|
||||||
|
console.error('No survey to open')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const creds = await getCreds().catch(handleError)
|
||||||
|
const userId = creds?.user_id
|
||||||
|
|
||||||
|
const formId = availableSurvey.value.tally_id
|
||||||
|
|
||||||
|
const popupOptions = {
|
||||||
|
layout: 'modal',
|
||||||
|
width: 700,
|
||||||
|
autoClose: 2000,
|
||||||
|
hideTitle: true,
|
||||||
|
hiddenFields: {
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
onOpen: () => console.info('Opened user survey'),
|
||||||
|
onClose: () => {
|
||||||
|
console.info('Closed user survey')
|
||||||
|
// show_ads_window()
|
||||||
|
},
|
||||||
|
onSubmit: () => console.info('Active user survey submitted'),
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// hide_ads_window()
|
||||||
|
if (window.Tally?.openPopup) {
|
||||||
|
console.info(`Opening Tally popup for user survey (form ID: ${formId})`)
|
||||||
|
dismissSurvey()
|
||||||
|
window.Tally.openPopup(formId, popupOptions)
|
||||||
|
} else {
|
||||||
|
console.warn('Tally script not yet loaded')
|
||||||
|
// show_ads_window()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error opening Tally popup:', e)
|
||||||
|
// show_ads_window()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`Found user survey to show with tally_id: ${formId}`)
|
||||||
|
window.Tally.openPopup(formId, popupOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissSurvey() {
|
||||||
|
localStorage.setItem(`survey-${availableSurvey.value.id}-display`, new Date())
|
||||||
|
availableSurvey.value = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processPendingSurveys() {
|
||||||
|
function isWithinLastTwoWeeks(date) {
|
||||||
|
const twoWeeksAgo = new Date()
|
||||||
|
twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14)
|
||||||
|
return date >= twoWeeksAgo
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupOldSurveyDisplayData()
|
||||||
|
|
||||||
|
const creds = await getCreds().catch(handleError)
|
||||||
|
const userId = creds?.user_id
|
||||||
|
|
||||||
|
const instances = await list().catch(handleError)
|
||||||
|
const isActivePlayer =
|
||||||
|
instances.findIndex(
|
||||||
|
(instance) =>
|
||||||
|
isWithinLastTwoWeeks(instance.last_played) && !isWithinLastTwoWeeks(instance.created),
|
||||||
|
) >= 0
|
||||||
|
|
||||||
|
let surveys = []
|
||||||
|
try {
|
||||||
|
surveys = await $fetch('https://api.modrinth.com/v2/surveys')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error fetching surveys:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const surveyToShow = surveys.find(
|
||||||
|
(survey) =>
|
||||||
|
!!(
|
||||||
|
localStorage.getItem(`survey-${survey.id}-display`) === null &&
|
||||||
|
survey.type === 'tally_app' &&
|
||||||
|
((survey.condition === 'active_player' && isActivePlayer) ||
|
||||||
|
(survey.assigned_users?.includes(userId) && !survey.dismissed_users?.includes(userId)))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (surveyToShow) {
|
||||||
|
availableSurvey.value = surveyToShow
|
||||||
|
} else {
|
||||||
|
console.info('No user survey to show')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -418,6 +558,9 @@ function handleAuxClick(e) {
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<AppSettingsModal ref="settingsModal" />
|
<AppSettingsModal ref="settingsModal" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
<Suspense>
|
||||||
|
<AuthGrantFlowWaitModal ref="modrinthLoginFlowWaitModal" @flow-cancel="cancelLogin" />
|
||||||
|
</Suspense>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<InstanceCreationModal ref="installationModal" />
|
<InstanceCreationModal ref="installationModal" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -465,12 +608,19 @@ function handleAuxClick(e) {
|
|||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<div class="flex flex-grow"></div>
|
<div class="flex flex-grow"></div>
|
||||||
<NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
|
<!-- <NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
|
</NavButton> -->
|
||||||
|
<template v-if="updateState">
|
||||||
|
<NavButton class="neon-icon pulse" v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||||
|
<SettingsIcon />
|
||||||
</NavButton>
|
</NavButton>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</NavButton>
|
</NavButton>
|
||||||
|
</template>
|
||||||
<ButtonStyled v-if="credentials" type="transparent" circular>
|
<ButtonStyled v-if="credentials" type="transparent" circular>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
@@ -501,13 +651,13 @@ function handleAuxClick(e) {
|
|||||||
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
|
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
|
||||||
<div class="flex items-center gap-1 ml-3">
|
<div class="flex items-center gap-1 ml-3">
|
||||||
<button
|
<button
|
||||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||||
@click="router.back()"
|
@click="router.back()"
|
||||||
>
|
>
|
||||||
<LeftArrowIcon />
|
<LeftArrowIcon />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||||
@click="router.forward()"
|
@click="router.forward()"
|
||||||
>
|
>
|
||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
@@ -559,6 +709,28 @@ function handleAuxClick(e) {
|
|||||||
:class="{ 'sidebar-enabled': sidebarVisible }"
|
:class="{ 'sidebar-enabled': sidebarVisible }"
|
||||||
>
|
>
|
||||||
<div class="app-viewport flex-grow router-view">
|
<div class="app-viewport flex-grow router-view">
|
||||||
|
<transition name="popup-survey">
|
||||||
|
<div
|
||||||
|
v-if="availableSurvey"
|
||||||
|
class="w-[400px] z-20 fixed -bottom-12 pb-16 right-[--right-bar-width] mr-4 rounded-t-2xl card-shadow bg-bg-raised border-divider border-[1px] border-solid border-b-0 p-4"
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-extrabold mt-0 mb-2">Hey there Modrinth user!</h2>
|
||||||
|
<p class="m-0 leading-tight">
|
||||||
|
Would you mind answering a few questions about your experience with Modrinth App?
|
||||||
|
</p>
|
||||||
|
<p class="mt-3 mb-4 leading-tight">
|
||||||
|
This feedback will go directly to the Modrinth team and help guide future updates!
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button @click="openSurvey"><NotepadTextIcon /> Take survey</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="dismissSurvey"><XIcon /> No thanks</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
<div
|
<div
|
||||||
class="loading-indicator-container h-8 fixed z-50"
|
class="loading-indicator-container h-8 fixed z-50"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -638,7 +810,7 @@ function handleAuxClick(e) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <template v-if="showAd">
|
<template v-if="showAd">
|
||||||
<a
|
<a
|
||||||
href="https://modrinth.plus?app"
|
href="https://modrinth.plus?app"
|
||||||
class="absolute bottom-[250px] w-full flex justify-center items-center gap-1 px-4 py-3 text-purple font-medium hover:underline z-10"
|
class="absolute bottom-[250px] w-full flex justify-center items-center gap-1 px-4 py-3 text-purple font-medium hover:underline z-10"
|
||||||
@@ -646,12 +818,12 @@ function handleAuxClick(e) {
|
|||||||
>
|
>
|
||||||
<ArrowBigUpDashIcon class="text-2xl" /> Upgrade to Modrinth+
|
<ArrowBigUpDashIcon class="text-2xl" /> Upgrade to Modrinth+
|
||||||
</a>
|
</a>
|
||||||
<PromotionWrapper />
|
<!-- <PromotionWrapper /> -->
|
||||||
</template> -->
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<URLConfirmModal ref="urlModal" />
|
<URLConfirmModal ref="urlModal" />
|
||||||
<Notifications ref="notificationsWrapper" sidebar />
|
<NotificationPanel has-sidebar />
|
||||||
<ErrorModal ref="errorModal" />
|
<ErrorModal ref="errorModal" />
|
||||||
<ModInstallModal ref="modInstallModal" />
|
<ModInstallModal ref="modInstallModal" />
|
||||||
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
||||||
@@ -659,6 +831,9 @@ function handleAuxClick(e) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
@import '../../../packages/assets/styles/neon-text.scss';
|
||||||
|
|
||||||
.window-controls {
|
.window-controls {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -856,6 +1031,26 @@ function handleAuxClick(e) {
|
|||||||
.sidebar-teleport-content:empty + .sidebar-default-content.sidebar-enabled {
|
.sidebar-teleport-content:empty + .sidebar-default-content.sidebar-enabled {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-survey-enter-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.25s ease,
|
||||||
|
transform 0.25s cubic-bezier(0.51, 1.08, 0.35, 1.15);
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-survey-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.25s ease,
|
||||||
|
transform 0.25s cubic-bezier(0.68, -0.17, 0.23, 0.11);
|
||||||
|
transform-origin: top center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-survey-enter-from,
|
||||||
|
.popup-survey-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10rem) scale(0.8) scaleY(1.6);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
.mac {
|
.mac {
|
||||||
|
|||||||
24
apps/app-frontend/src/assets/external/index.js
vendored
24
apps/app-frontend/src/assets/external/index.js
vendored
@@ -1,18 +1,18 @@
|
|||||||
|
export { default as ATLauncherIcon } from './atlauncher.svg'
|
||||||
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
export { default as BuyMeACoffeeIcon } from './bmac.svg'
|
||||||
export { default as DiscordIcon } from './discord.svg'
|
export { default as DiscordIcon } from './discord.svg'
|
||||||
|
export { default as GDLauncherIcon } from './gdlauncher.png'
|
||||||
|
export { default as GithubIcon } from './github.svg'
|
||||||
|
export { default as GitLabIcon } from './gitlab.svg'
|
||||||
|
export { default as GoogleIcon } from './google.svg'
|
||||||
export { default as KoFiIcon } from './kofi.svg'
|
export { default as KoFiIcon } from './kofi.svg'
|
||||||
|
export { default as MastodonIcon } from './mastodon.svg'
|
||||||
|
export { default as MicrosoftIcon } from './microsoft.svg'
|
||||||
|
export { default as MultiMCIcon } from './multimc.webp'
|
||||||
|
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
||||||
export { default as PatreonIcon } from './patreon.svg'
|
export { default as PatreonIcon } from './patreon.svg'
|
||||||
export { default as PaypalIcon } from './paypal.svg'
|
export { default as PaypalIcon } from './paypal.svg'
|
||||||
export { default as OpenCollectiveIcon } from './opencollective.svg'
|
|
||||||
export { default as TwitterIcon } from './twitter.svg'
|
|
||||||
export { default as GithubIcon } from './github.svg'
|
|
||||||
export { default as MastodonIcon } from './mastodon.svg'
|
|
||||||
export { default as RedditIcon } from './reddit.svg'
|
|
||||||
export { default as GoogleIcon } from './google.svg'
|
|
||||||
export { default as MicrosoftIcon } from './microsoft.svg'
|
|
||||||
export { default as SteamIcon } from './steam.svg'
|
|
||||||
export { default as GitLabIcon } from './gitlab.svg'
|
|
||||||
export { default as ATLauncherIcon } from './atlauncher.svg'
|
|
||||||
export { default as GDLauncherIcon } from './gdlauncher.png'
|
|
||||||
export { default as MultiMCIcon } from './multimc.webp'
|
|
||||||
export { default as PrismIcon } from './prism.svg'
|
export { default as PrismIcon } from './prism.svg'
|
||||||
|
export { default as RedditIcon } from './reddit.svg'
|
||||||
|
export { default as SteamIcon } from './steam.svg'
|
||||||
|
export { default as TwitterIcon } from './twitter.svg'
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export { default as SwapIcon } from './arrow-left-right.svg'
|
|
||||||
export { default as ToggleIcon } from './toggle.svg'
|
|
||||||
export { default as PackageIcon } from './package.svg'
|
|
||||||
export { default as VersionIcon } from './milestone.svg'
|
|
||||||
export { default as TextInputIcon } from './text-cursor-input.svg'
|
|
||||||
export { default as AddProjectImage } from './add-project.svg'
|
export { default as AddProjectImage } from './add-project.svg'
|
||||||
export { default as NewInstanceImage } from './new-instance.svg'
|
export { default as SwapIcon } from './arrow-left-right.svg'
|
||||||
export { default as MenuIcon } from './menu.svg'
|
export { default as MenuIcon } from './menu.svg'
|
||||||
export { default as ChatIcon } from './messages-square.svg'
|
export { default as ChatIcon } from './messages-square.svg'
|
||||||
export { default as Pirate } from './pirate.svg'
|
export { default as Pirate } from './pirate.svg'
|
||||||
export { default as Microsoft } from './microsoft.svg'
|
export { default as Microsoft } from './microsoft.svg'
|
||||||
export { default as PirateShip } from './pirate-ship.svg'
|
export { default as PirateShip } from './pirate-ship.svg'
|
||||||
|
export { default as VersionIcon } from './milestone.svg'
|
||||||
|
export { default as NewInstanceImage } from './new-instance.svg'
|
||||||
|
export { default as PackageIcon } from './package.svg'
|
||||||
|
export { default as TextInputIcon } from './text-cursor-input.svg'
|
||||||
|
export { default as ToggleIcon } from './toggle.svg'
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
TrashIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
EyeIcon,
|
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TrashIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, DropdownSelect } from '@modrinth/ui'
|
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { formatCategoryHeader } from '@modrinth/utils'
|
import { formatCategoryHeader } from '@modrinth/utils'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { duplicate, remove } from '@/helpers/profile.js'
|
import { computed, ref } from 'vue'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { duplicate, remove } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
@@ -136,7 +138,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
if (sortBy.value === 'Game version') {
|
if (sortBy.value === 'Game version') {
|
||||||
instances.sort((a, b) => {
|
instances.sort((a, b) => {
|
||||||
return a.game_version.localeCompare(b.game_version)
|
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +215,17 @@ const filteredResults = computed(() => {
|
|||||||
instanceMap.set(entry[0], entry[1])
|
instanceMap.set(entry[0], entry[1])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
||||||
|
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
||||||
|
if (group.value === 'Game version') {
|
||||||
|
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||||
|
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
||||||
|
})
|
||||||
|
instanceMap.clear()
|
||||||
|
sortedEntries.forEach((entry) => {
|
||||||
|
instanceMap.set(entry[0], entry[1])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return instanceMap
|
return instanceMap
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useLoading } from '@/store/state.js'
|
import { useLoading } from '@/store/state.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
FolderOpenIcon,
|
|
||||||
PlayIcon,
|
|
||||||
PlusIcon,
|
|
||||||
TrashIcon,
|
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
GlobeIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
ExternalIcon,
|
ExternalIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
PlayIcon,
|
||||||
|
PlusIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
TrashIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process.js'
|
||||||
|
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
import { install as installVersion } from '@/store/install.js'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import { HeadingLink } from '@modrinth/ui'
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -163,7 +165,14 @@ const handleOptionsClick = async (args) => {
|
|||||||
await navigator.clipboard.writeText(args.item.path)
|
await navigator.clipboard.writeText(args.item.path)
|
||||||
break
|
break
|
||||||
case 'install': {
|
case 'install': {
|
||||||
await installVersion(args.item.project_id, null, null, 'ProjectCardContextMenu')
|
await installVersion(
|
||||||
|
args.item.project_id,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
'ProjectCardContextMenu',
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
).catch(handleError)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-if="mode !== 'isolated'" ref="button"
|
||||||
v-if="mode !== 'isolated'"
|
|
||||||
ref="button"
|
|
||||||
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||||
:class="{ expanded: mode === 'expanded' }"
|
:class="{ expanded: mode === 'expanded' }" @click="toggleMenu">
|
||||||
@click="toggleMenu"
|
<Avatar size="36px" :src="selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
>
|
" />
|
||||||
<Avatar
|
|
||||||
size="36px"
|
|
||||||
:src="
|
|
||||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<span>
|
<span>
|
||||||
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
|
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
|
||||||
@@ -28,35 +20,28 @@
|
|||||||
<Avatar size="xs" :src="avatarUrl" />
|
<Avatar size="xs" :src="avatarUrl" />
|
||||||
<div>
|
<div>
|
||||||
<h4>
|
<h4>
|
||||||
<component :is="getAccountType(selectedAccount)" class="vector-icon" /> {{ selectedAccount.profile.name }}
|
<component :is="getAccountType(selectedAccount)" class="vector-icon" /> {{
|
||||||
|
selectedAccount.profile.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<p>Selected</p>
|
<p>Selected</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.profile.id)">
|
||||||
v-tooltip="'Log out'"
|
|
||||||
icon-only
|
|
||||||
color="raised"
|
|
||||||
@click="logout(selectedAccount.profile.id)"
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="login-section account">
|
<div v-else class="login-section account">
|
||||||
<h4>Not signed in</h4>
|
<h4>Not signed in</h4>
|
||||||
<Button
|
<Button v-tooltip="'Log via Microsoft'" :disabled="microsoftLoginDisabled" icon-only @click="login()">
|
||||||
v-tooltip="'Log in'"
|
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
|
||||||
:disabled="loginDisabled"
|
|
||||||
icon-only
|
|
||||||
color="primary"
|
|
||||||
@click="login()"
|
|
||||||
>
|
|
||||||
<LogInIcon v-if="!loginDisabled" />
|
|
||||||
<SpinnerIcon v-else class="animate-spin" />
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
<MicrosoftIcon/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
|
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
|
||||||
<PirateIcon />
|
<PirateIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
|
||||||
|
<ElyByIcon v-if="!elybyLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
|
</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.profile.id" class="account-row">
|
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||||
@@ -73,35 +58,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="accounts.length > 0" class="login-section account centered">
|
<div v-if="accounts.length > 0" class="login-section account centered">
|
||||||
<Button v-tooltip="'Log in'" icon-only @click="login()">
|
<Button v-tooltip="'Log via Microsoft'" icon-only @click="login()">
|
||||||
<MicrosoftIcon />
|
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
|
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
|
||||||
<PirateIcon />
|
<PirateIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
|
||||||
|
<ElyByIcon v-if="!elybyLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</transition>
|
</transition>
|
||||||
<ModalWrapper ref="loginOfflineModal" class="modal" header="Add new offline account">
|
<ModalWrapper ref="addElybyModal" class="modal" header="Authenticate with Ely.by">
|
||||||
<div class="modal-body">
|
<ModalWrapper ref="requestElybyTwoFactorCodeModal" class="modal"
|
||||||
<div class="label">Enter offline username</div>
|
header="Ely.by requested 2FA code for authentication">
|
||||||
<input type="text" v-model="playerName" placeholder="Provide offline player name" />
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
<Button icon-only color="secondary" @click="offlineLoginFinally()">
|
<label class="label">Enter your 2FA code</label>
|
||||||
|
<input v-model="elybyTwoFactorCode" type="text" placeholder="Your 2FA code here..." class="input" />
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="loginErrorModal" class="modal" header="Error while proceed">
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
<div class="modal-body">
|
<label class="label">Enter your player name or email (preferred)</label>
|
||||||
<div class="label">Error occurred while adding offline account</div>
|
<input v-model="elybyLogin" type="text" placeholder="Your player name or email here..." class="input" />
|
||||||
<Button color="primary" @click="retryOfflineLogin()">
|
<label class="label">Enter your password</label>
|
||||||
|
<input v-model="elybyPassword" type="password" placeholder="Your password here..." class="input" />
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="addOfflineModal" class="modal" header="Add new offline account">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="label">Enter your player name</label>
|
||||||
|
<input v-model="offlinePlayerName" type="text" placeholder="Your player name here..." class="input" />
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addOfflineProfile()">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="authenticationElybyErrorModal" class="modal"
|
||||||
|
header="Error while proceeding authentication event with Ely.by">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while logging in.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
|
||||||
Try again
|
Try again
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="unexpectedErrorModal" class="modal" header="Ошибка">
|
<ModalWrapper ref="inputElybyErrorModal" class="modal" header="Error while proceeding input event with Ely.by">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while adding the Ely.by account. Please follow the instructions below.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>Check that you have entered the correct player name or email.</li>
|
||||||
|
<li>Check that you have entered the correct password.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="inputErrorModal" class="modal" header="Error while proceeding input event with offline account">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while adding the offline account. Please follow the instructions below.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>Check that you have entered the correct player name.</li>
|
||||||
|
<li>
|
||||||
|
Player name must be at least {{ minOfflinePlayerNameLength }} characters long and no more than
|
||||||
|
{{ maxOfflinePlayerNameLength }} characters.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddOfflineProfile">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="exceptionErrorModal" class="modal" header="Unexpected error occurred">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="label">Unexcepted error</div>
|
<label class="label">An unexpected error has occurred. Please try again later.</label>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
@@ -109,17 +171,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
PlusIcon,
|
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
LogInIcon,
|
|
||||||
PirateIcon as Offline,
|
PirateIcon as Offline,
|
||||||
MicrosoftIcon as License,
|
MicrosoftIcon as License,
|
||||||
|
ElyByIcon as Elyby,
|
||||||
MicrosoftIcon,
|
MicrosoftIcon,
|
||||||
PirateIcon,
|
PirateIcon,
|
||||||
SpinnerIcon } from '@modrinth/assets'
|
ElyByIcon,
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
SpinnerIcon
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
elyby_auth_authenticate,
|
||||||
|
elyby_login,
|
||||||
offline_login,
|
offline_login,
|
||||||
users,
|
users,
|
||||||
remove_user,
|
remove_user,
|
||||||
@@ -127,13 +192,14 @@ import {
|
|||||||
login as login_flow,
|
login as login_flow,
|
||||||
get_default_user,
|
get_default_user,
|
||||||
} from '@/helpers/auth'
|
} from '@/helpers/auth'
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_available_skins } from '@/helpers/skins'
|
|
||||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||||
|
import { get_available_skins } from '@/helpers/skins'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
@@ -146,48 +212,180 @@ defineProps({
|
|||||||
const emit = defineEmits(['change'])
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
const accounts = ref({})
|
const accounts = ref({})
|
||||||
const loginDisabled = ref(false)
|
const microsoftLoginDisabled = ref(false)
|
||||||
|
const elybyLoginDisabled = ref(false)
|
||||||
const defaultUser = ref()
|
const defaultUser = ref()
|
||||||
const loginOfflineModal = ref(null)
|
|
||||||
const loginErrorModal = ref(null)
|
|
||||||
const unexpectedErrorModal = ref(null)
|
|
||||||
const playerName = ref('')
|
|
||||||
|
|
||||||
async function tryOfflineLogin() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
loginOfflineModal.value.show()
|
const clientToken = "astralrinth"
|
||||||
|
const addOfflineModal = ref(null)
|
||||||
|
const addElybyModal = ref(null)
|
||||||
|
const requestElybyTwoFactorCodeModal = ref(null)
|
||||||
|
const authenticationElybyErrorModal = ref(null)
|
||||||
|
const inputElybyErrorModal = ref(null)
|
||||||
|
const inputErrorModal = ref(null)
|
||||||
|
const exceptionErrorModal = ref(null)
|
||||||
|
const offlinePlayerName = ref('')
|
||||||
|
const elybyLogin = ref('')
|
||||||
|
const elybyPassword = ref('')
|
||||||
|
const elybyTwoFactorCode = ref('')
|
||||||
|
const minOfflinePlayerNameLength = 2
|
||||||
|
const maxOfflinePlayerNameLength = 20
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function getAccountType(account) {
|
||||||
|
switch (account.account_type) {
|
||||||
|
case 'microsoft':
|
||||||
|
return License
|
||||||
|
case 'pirate':
|
||||||
|
return Offline
|
||||||
|
case 'elyby':
|
||||||
|
return Elyby
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function offlineLoginFinally() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
const name = playerName.value
|
function showOfflineLoginModal() {
|
||||||
if (name.length > 1 && name.length < 20 && name !== '') {
|
addOfflineModal.value?.show()
|
||||||
const loggedIn = await offline_login(name).catch(handleError)
|
}
|
||||||
loginOfflineModal.value.hide()
|
|
||||||
if (loggedIn) {
|
// [AR] • Feature
|
||||||
await setAccount(loggedIn)
|
function showElybyLoginModal() {
|
||||||
|
addElybyModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function retryAddOfflineProfile() {
|
||||||
|
inputErrorModal.value?.hide()
|
||||||
|
clearOfflineFields()
|
||||||
|
showOfflineLoginModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function retryAddElybyProfile() {
|
||||||
|
authenticationElybyErrorModal.value?.hide()
|
||||||
|
inputElybyErrorModal.value?.hide()
|
||||||
|
clearElybyFields()
|
||||||
|
showElybyLoginModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function clearElybyFields() {
|
||||||
|
elybyLogin.value = ''
|
||||||
|
elybyPassword.value = ''
|
||||||
|
elybyTwoFactorCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function clearOfflineFields() {
|
||||||
|
offlinePlayerName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
async function addOfflineProfile() {
|
||||||
|
const name = offlinePlayerName.value.trim()
|
||||||
|
const isValidName = name.length >= minOfflinePlayerNameLength && name.length <= maxOfflinePlayerNameLength
|
||||||
|
|
||||||
|
if (!isValidName) {
|
||||||
|
addOfflineModal.value?.hide()
|
||||||
|
inputErrorModal.value?.show()
|
||||||
|
clearOfflineFields()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await offline_login(name)
|
||||||
|
|
||||||
|
addOfflineModal.value?.hide()
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
await setAccount(result)
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
} else {
|
} else {
|
||||||
unexpectedErrorModal.value.show()
|
exceptionErrorModal.value?.show()
|
||||||
}
|
}
|
||||||
playerName.value = ''
|
} catch (error) {
|
||||||
} else {
|
handleError(error)
|
||||||
playerName.value = ''
|
exceptionErrorModal.value?.show()
|
||||||
loginOfflineModal.value.hide()
|
} finally {
|
||||||
loginErrorModal.value.show()
|
clearOfflineFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retryOfflineLogin() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
loginErrorModal.value.hide()
|
async function addElybyProfile() {
|
||||||
tryOfflineLogin()
|
if (!elybyLogin.value || !elybyPassword.value) {
|
||||||
}
|
addElybyModal.value?.hide()
|
||||||
|
inputElybyErrorModal.value?.show()
|
||||||
|
clearElybyFields()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elybyLoginDisabled.value = true
|
||||||
|
|
||||||
function getAccountType(account) { // Patched by AstralRinth
|
const login = elybyLogin.value.trim()
|
||||||
if (account.access_token != "null" && account.access_token != null && account.access_token != "") {
|
let password = elybyPassword.value.trim()
|
||||||
return License
|
const twoFactorCode = elybyTwoFactorCode.value.trim()
|
||||||
} else {
|
if (password && twoFactorCode) {
|
||||||
return Offline
|
password = `${password}:${twoFactorCode}`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw_result = await elyby_auth_authenticate(
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
clientToken
|
||||||
|
)
|
||||||
|
|
||||||
|
const json_data = JSON.parse(raw_result)
|
||||||
|
|
||||||
|
console.log(json_data?.error)
|
||||||
|
console.log(json_data?.errorMessage)
|
||||||
|
|
||||||
|
if (!json_data.accessToken) {
|
||||||
|
if (
|
||||||
|
json_data.error === 'ForbiddenOperationException' &&
|
||||||
|
json_data.errorMessage?.includes('two factor')
|
||||||
|
) {
|
||||||
|
requestElybyTwoFactorCodeModal.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addElybyModal.value?.hide()
|
||||||
|
requestElybyTwoFactorCodeModal.value?.hide()
|
||||||
|
authenticationElybyErrorModal.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = json_data.accessToken
|
||||||
|
const selectedProfileId = convertRawStringToUUIDv4(json_data.selectedProfile.id)
|
||||||
|
const selectedProfileName = json_data.selectedProfile.name
|
||||||
|
|
||||||
|
const result = await elyby_login(selectedProfileId, selectedProfileName, accessToken)
|
||||||
|
|
||||||
|
addElybyModal.value?.hide()
|
||||||
|
requestElybyTwoFactorCodeModal.value?.hide()
|
||||||
|
|
||||||
|
clearElybyFields()
|
||||||
|
|
||||||
|
await setAccount(result)
|
||||||
|
await refreshValues()
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err)
|
||||||
|
exceptionErrorModal.value?.show()
|
||||||
|
} finally {
|
||||||
|
elybyLoginDisabled.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function convertRawStringToUUIDv4(rawId) {
|
||||||
|
if (rawId.length !== 32) {
|
||||||
|
console.warn('Invalid UUID string:', rawId)
|
||||||
|
return rawId
|
||||||
|
}
|
||||||
|
return `${rawId.slice(0, 8)}-${rawId.slice(8, 12)}-${rawId.slice(12, 16)}-${rawId.slice(16, 20)}-${rawId.slice(20)}`
|
||||||
|
}
|
||||||
|
|
||||||
const equippedSkin = ref(null)
|
const equippedSkin = ref(null)
|
||||||
const headUrlCache = ref(new Map())
|
const headUrlCache = ref(new Map())
|
||||||
|
|
||||||
@@ -213,13 +411,13 @@ async function refreshValues() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setLoginDisabled(value) {
|
function setLoginDisabled(value) {
|
||||||
loginDisabled.value = value
|
microsoftLoginDisabled.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refreshValues,
|
refreshValues,
|
||||||
setLoginDisabled,
|
setLoginDisabled,
|
||||||
loginDisabled,
|
microsoftLoginDisabled,
|
||||||
})
|
})
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
|
|
||||||
@@ -265,7 +463,7 @@ async function setAccount(account) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
loginDisabled.value = true
|
microsoftLoginDisabled.value = true
|
||||||
const loggedIn = await login_flow().catch(handleSevereError)
|
const loggedIn = await login_flow().catch(handleSevereError)
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
@@ -274,7 +472,7 @@ async function login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('AccountLogIn')
|
trackEvent('AccountLogIn')
|
||||||
loginDisabled.value = false
|
microsoftLoginDisabled.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = async (id) => {
|
const logout = async (id) => {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
|
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { add_project_from_path } from '@/helpers/profile.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { add_project_from_path } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -42,11 +42,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from '@modrinth/assets'
|
import { ChevronLeftIcon, ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button } from '@modrinth/ui'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-show="shown" ref="contextMenu" class="context-menu" :style="{
|
<div
|
||||||
|
v-show="shown"
|
||||||
|
ref="contextMenu"
|
||||||
|
class="context-menu"
|
||||||
|
:style="{
|
||||||
left: left,
|
left: left,
|
||||||
top: top,
|
top: top,
|
||||||
}">
|
}"
|
||||||
|
>
|
||||||
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
<div v-for="(option, index) in options" :key="index" @click.stop="optionClicked(option.name)">
|
||||||
<hr v-if="option.type === 'divider'" class="divider" />
|
<hr v-if="option.type === 'divider'" class="divider" />
|
||||||
<div v-else-if="!(isLinkedData(item) && option.name === `add_content`)" class="item clickable"
|
<div
|
||||||
:class="[option.color ?? 'base']">
|
v-else-if="!(isLinkedData(item) && option.name === `add_content`)"
|
||||||
|
class="item clickable"
|
||||||
|
:class="[option.color ?? 'base']"
|
||||||
|
>
|
||||||
<slot :name="option.name" />
|
<slot :name="option.name" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,380 @@
|
|||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal" :header="'Import from CurseForge Profile Code'">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="input-row">
|
||||||
|
<p class="input-label">Profile Code</p>
|
||||||
|
<div class="iconified-input">
|
||||||
|
<SearchIcon aria-hidden="true" class="text-lg" />
|
||||||
|
<input ref="codeInput" v-model="profileCode" autocomplete="off" class="h-12 card-shadow"
|
||||||
|
spellcheck="false" type="text" placeholder="Enter CurseForge profile code" maxlength="20"
|
||||||
|
@keyup.enter="importProfile" />
|
||||||
|
<Button v-if="profileCode" class="r-btn" @click="() => (profileCode = '')">
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="metadata && !importing" class="profile-info">
|
||||||
|
<h3>Profile Information</h3>
|
||||||
|
<p><strong>Name:</strong> {{ metadata.name }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="importing && importProgress.visible" class="progress-section">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span class="progress-text">{{ importProgress.message }}</span>
|
||||||
|
<span class="progress-percentage">{{ Math.floor(importProgress.percentage) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" :style="{ width: `${importProgress.percentage}%` }"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
|
<Button @click="hide" :disabled="importing">
|
||||||
|
<XIcon />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button v-if="!metadata" @click="fetchMetadata" :disabled="!profileCode.trim() || fetching"
|
||||||
|
color="secondary">
|
||||||
|
<SearchIcon v-if="!fetching" />
|
||||||
|
{{ fetching ? 'Checking...' : 'Check Profile' }}
|
||||||
|
</Button>
|
||||||
|
<Button v-if="metadata" @click="importProfile" :disabled="importing" color="primary">
|
||||||
|
<DownloadIcon v-if="!importing" />
|
||||||
|
{{ importing ? 'Importing...' : 'Import Profile' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { Button } from '@modrinth/ui'
|
||||||
|
import {
|
||||||
|
XIcon,
|
||||||
|
SearchIcon,
|
||||||
|
DownloadIcon
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
fetch_curseforge_profile_metadata,
|
||||||
|
import_curseforge_profile
|
||||||
|
} from '@/helpers/import.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { loading_listener } from '@/helpers/events.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
closeParent: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const modal = ref(null)
|
||||||
|
const codeInput = ref(null)
|
||||||
|
const profileCode = ref('')
|
||||||
|
const metadata = ref(null)
|
||||||
|
const fetching = ref(false)
|
||||||
|
const importing = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const importProgress = ref({
|
||||||
|
visible: false,
|
||||||
|
percentage: 0,
|
||||||
|
message: 'Starting import...',
|
||||||
|
totalMods: 0,
|
||||||
|
downloadedMods: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
let unlistenLoading = null
|
||||||
|
let activeLoadingBarId = null
|
||||||
|
let progressFallbackTimer = null
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show: () => {
|
||||||
|
profileCode.value = ''
|
||||||
|
metadata.value = null
|
||||||
|
fetching.value = false
|
||||||
|
importing.value = false
|
||||||
|
error.value = ''
|
||||||
|
importProgress.value = {
|
||||||
|
visible: false,
|
||||||
|
percentage: 0,
|
||||||
|
message: 'Starting import...',
|
||||||
|
totalMods: 0,
|
||||||
|
downloadedMods: 0
|
||||||
|
}
|
||||||
|
modal.value?.show()
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
codeInput.value?.focus()
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
trackEvent('CurseForgeProfileImportStart', { source: 'ImportModal' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
modal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchMetadata = async () => {
|
||||||
|
if (!profileCode.value.trim()) return
|
||||||
|
|
||||||
|
fetching.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fetch_curseforge_profile_metadata(profileCode.value.trim())
|
||||||
|
metadata.value = result
|
||||||
|
trackEvent('CurseForgeProfileMetadataFetched', {
|
||||||
|
profileCode: profileCode.value.trim()
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch CurseForge profile metadata:', err)
|
||||||
|
error.value = 'Failed to fetch profile information. Please check the code and try again.'
|
||||||
|
handleError(err)
|
||||||
|
} finally {
|
||||||
|
fetching.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importProfile = async () => {
|
||||||
|
if (!profileCode.value.trim()) return
|
||||||
|
|
||||||
|
importing.value = true
|
||||||
|
error.value = ''
|
||||||
|
activeLoadingBarId = null // Reset for new import session
|
||||||
|
importProgress.value = {
|
||||||
|
visible: true,
|
||||||
|
percentage: 0,
|
||||||
|
message: 'Starting import...',
|
||||||
|
totalMods: 0,
|
||||||
|
downloadedMods: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback progress timer in case loading events don't work
|
||||||
|
progressFallbackTimer = setInterval(() => {
|
||||||
|
if (importing.value && importProgress.value.percentage < 90) {
|
||||||
|
// Slowly increment progress as a fallback
|
||||||
|
importProgress.value.percentage = Math.min(90, importProgress.value.percentage + 1)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { result, profilePath } = await import_curseforge_profile(profileCode.value.trim())
|
||||||
|
|
||||||
|
trackEvent('CurseForgeProfileImported', {
|
||||||
|
profileCode: profileCode.value.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
hide()
|
||||||
|
|
||||||
|
// Close the parent modal if provided
|
||||||
|
if (props.closeParent) {
|
||||||
|
props.closeParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to the imported profile
|
||||||
|
await router.push(`/instance/${encodeURIComponent(profilePath)}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to import CurseForge profile:', err)
|
||||||
|
error.value = 'Failed to import profile. Please try again.'
|
||||||
|
handleError(err)
|
||||||
|
} finally {
|
||||||
|
importing.value = false
|
||||||
|
importProgress.value.visible = false
|
||||||
|
if (progressFallbackTimer) {
|
||||||
|
clearInterval(progressFallbackTimer)
|
||||||
|
progressFallbackTimer = null
|
||||||
|
}
|
||||||
|
activeLoadingBarId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Listen for loading events to update progress
|
||||||
|
unlistenLoading = await loading_listener((event) => {
|
||||||
|
console.log('Loading event received:', event) // Debug log
|
||||||
|
|
||||||
|
// Handle all loading events that could be related to CurseForge profile import
|
||||||
|
const isCurseForgeEvent = event.event?.type === 'curseforge_profile_download'
|
||||||
|
const hasProfileName = event.event?.profile_name && importing.value
|
||||||
|
|
||||||
|
if ((isCurseForgeEvent || hasProfileName) && importing.value) {
|
||||||
|
// Store the loading bar ID for this import session
|
||||||
|
if (!activeLoadingBarId) {
|
||||||
|
activeLoadingBarId = event.loader_uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process events for our current import session
|
||||||
|
if (event.loader_uuid === activeLoadingBarId) {
|
||||||
|
if (event.fraction !== null && event.fraction !== undefined) {
|
||||||
|
const baseProgress = (event.fraction || 0) * 100
|
||||||
|
|
||||||
|
// Calculate custom progress based on the message
|
||||||
|
let finalProgress = baseProgress
|
||||||
|
const message = event.message || 'Importing profile...'
|
||||||
|
|
||||||
|
// Custom progress calculation for different stages
|
||||||
|
if (message.includes('Fetching') || message.includes('metadata')) {
|
||||||
|
finalProgress = Math.min(10, baseProgress)
|
||||||
|
} else if (message.includes('Downloading profile ZIP') || message.includes('profile ZIP')) {
|
||||||
|
finalProgress = Math.min(15, 10 + (baseProgress - 10) * 0.5)
|
||||||
|
} else if (message.includes('Extracting') || message.includes('ZIP')) {
|
||||||
|
finalProgress = Math.min(20, 15 + (baseProgress - 15) * 0.5)
|
||||||
|
} else if (message.includes('Configuring') || message.includes('profile')) {
|
||||||
|
finalProgress = Math.min(30, 20 + (baseProgress - 20) * 0.5)
|
||||||
|
} else if (message.includes('Copying') || message.includes('files')) {
|
||||||
|
finalProgress = Math.min(40, 30 + (baseProgress - 30) * 0.5)
|
||||||
|
} else if (message.includes('Downloaded mod') && message.includes(' of ')) {
|
||||||
|
// Parse "Downloaded mod X of Y" message
|
||||||
|
const match = message.match(/Downloaded mod (\d+) of (\d+)/)
|
||||||
|
if (match) {
|
||||||
|
const current = parseInt(match[1])
|
||||||
|
const total = parseInt(match[2])
|
||||||
|
// Mods take 40% of progress (from 40% to 80%)
|
||||||
|
const modProgress = (current / total) * 40
|
||||||
|
finalProgress = 40 + modProgress
|
||||||
|
} else {
|
||||||
|
finalProgress = Math.min(80, 40 + (baseProgress - 40) * 0.5)
|
||||||
|
}
|
||||||
|
} else if (message.includes('Downloading mod') || message.includes('mods')) {
|
||||||
|
// General mod downloading stage (40% to 80%)
|
||||||
|
finalProgress = Math.min(80, 40 + (baseProgress - 40) * 0.4)
|
||||||
|
} else if (message.includes('Installing Minecraft') || message.includes('Minecraft')) {
|
||||||
|
finalProgress = Math.min(95, 80 + (baseProgress - 80) * 0.75)
|
||||||
|
} else if (message.includes('Finalizing') || message.includes('completed')) {
|
||||||
|
finalProgress = Math.min(100, 95 + (baseProgress - 95))
|
||||||
|
} else {
|
||||||
|
// Default: use the base progress but ensure minimum progression
|
||||||
|
finalProgress = Math.max(importProgress.value.percentage, baseProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
importProgress.value.percentage = Math.min(100, Math.max(0, finalProgress))
|
||||||
|
importProgress.value.message = message
|
||||||
|
} else {
|
||||||
|
// Loading complete
|
||||||
|
importProgress.value.percentage = 100
|
||||||
|
importProgress.value.message = 'Import completed!'
|
||||||
|
activeLoadingBarId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unlistenLoading) {
|
||||||
|
unlistenLoading()
|
||||||
|
}
|
||||||
|
if (progressFallbackTimer) {
|
||||||
|
clearInterval(progressFallbackTimer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-contrast);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 1px solid var(--color-button);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--color-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
color: var(--color-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: var(--color-red);
|
||||||
|
border: 1px solid var(--color-red);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0.75rem;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-contrast);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
color: var(--color-base);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-percentage {
|
||||||
|
color: var(--color-contrast);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--color-button);
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-brand);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,28 +1,37 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
CopyIcon,
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
XIcon,
|
|
||||||
HammerIcon,
|
HammerIcon,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
CopyIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { ChatIcon } from '@/assets/icons'
|
import { ChatIcon } from '@/assets/icons'
|
||||||
import { ButtonStyled, Collapsible } from '@modrinth/ui'
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
|
||||||
import { install } from '@/helpers/profile.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
|
import { install } from '@/helpers/profile.js'
|
||||||
|
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
|
// [AR] Feature
|
||||||
|
import { applyMigrationFix } from '@/helpers/utils.js'
|
||||||
|
import { restartApp } from '@/helpers/utils.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
const error = ref()
|
const error = ref()
|
||||||
const closable = ref(true)
|
const closable = ref(true)
|
||||||
const errorCollapsed = ref(false)
|
const errorCollapsed = ref(false)
|
||||||
|
const language = ref('en')
|
||||||
|
const migrationFixSuccess = ref(null) // null | true | false
|
||||||
|
const migrationFixCallbackModel = ref()
|
||||||
|
|
||||||
const title = ref('An error occurred')
|
const title = ref('An error occurred')
|
||||||
const errorType = ref('unknown')
|
const errorType = ref('unknown')
|
||||||
@@ -148,6 +157,30 @@ async function copyToClipboard(text) {
|
|||||||
copied.value = false
|
copied.value = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleLanguage() {
|
||||||
|
language.value = language.value === 'en' ? 'ru' : 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onApplyMigrationFix(eol) {
|
||||||
|
console.log(`[AR] • Attempting to apply migration ${eol.toUpperCase()} fix`)
|
||||||
|
try {
|
||||||
|
const result = await applyMigrationFix(eol)
|
||||||
|
migrationFixSuccess.value = result === true
|
||||||
|
console.log(`[AR] • Successfully applied migration ${eol.toUpperCase()} fix`, result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[AR] • Failed to apply migration fix:`, err)
|
||||||
|
migrationFixSuccess.value = false
|
||||||
|
} finally {
|
||||||
|
migrationFixCallbackModel.value?.show?.()
|
||||||
|
if (migrationFixSuccess.value === true) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await restartApp()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -313,10 +346,120 @@ async function copyToClipboard(text) {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<Collapsible :collapsed="errorCollapsed">
|
<Collapsible :collapsed="errorCollapsed">
|
||||||
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre>
|
<pre class="m-0 px-4 py-3 bg-bg rounded-none whitespace-pre-wrap break-words overflow-x-auto max-w-full"
|
||||||
|
>{{ debugInfo }}</pre>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="errorType === 'state_init'">
|
||||||
|
<div class="notice">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 v-if="language === 'en'" class="notice__title">⚠️ Migration Issue • Important Notice ⚠️</h3>
|
||||||
|
<h3 v-if="language === 'ru'" class="notice__title">⚠️ Проблема миграции • Важное уведомление ⚠️</h3>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="toggleLanguage">
|
||||||
|
{{ language === 'en' ? '📖 Русский' : '📖 English' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
We're experiencing an issue with our database migration system due to differences in how different operating systems handle line endings. This might cause problems with our app's functionality.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
<strong>What's happening?</strong> When we build our app, we use a system that checks the integrity of our database migrations. However, this system can get confused when it encounters different line endings (like CRLF vs LF) used by different operating systems. This can lead to errors and make our app unusable.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
<strong>Why is this happening?</strong> This issue is caused by a combination of factors, including different operating systems handling line endings differently, Git's line ending conversion settings, and our app's build process.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
<strong>What are we doing about it?</strong> We're working to resolve this issue and ensure that our app works smoothly for all users. In the meantime, we apologize for any inconvenience this might cause and appreciate your patience and understanding.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
Мы сталкиваемся с проблемой в нашей системе миграции базы данных из-за различий в том, как разные операционные системы обрабатывают окончания строк. Это может вызвать проблемы с функциональностью нашего приложения.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
<strong>Что происходит?</strong> Когда мы строим наше приложение, мы используем систему, которая проверяет целостность наших миграций базы данных. Однако эта система может сбиваться, когда сталкивается с различными окончаниями строк (например, CRLF против LF), используемыми разными операционными системами. Это может привести к ошибкам и сделать наше приложение неработоспособным.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
<strong>Почему это происходит?</strong> Эта проблема вызвана сочетанием факторов, включая различную обработку окончаний строк разными операционными системами, настройки преобразования окончаний строк в Git и процесс сборки нашего приложения.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
<strong>Что мы с этим делаем?</strong> Мы работаем над решением этой проблемы и обеспечением бесперебойной работы нашего приложения для всех пользователей. В это время мы извиняемся за возможные неудобства и благодарим вас за терпение и понимание.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg font-bold text-contrast">
|
||||||
|
<template v-if="language === 'en'">Possible fix in real time:</template>
|
||||||
|
<template v-if="language === 'ru'">Возможное исправление в реальном времени:</template>
|
||||||
|
</h2>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<ol class="flex flex-col gap-3">
|
||||||
|
<li>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button
|
||||||
|
:title="language === 'en'
|
||||||
|
? 'Convert all line endings in migration files to LF (Unix-style: \\n)'
|
||||||
|
: 'Преобразовать все окончания строк в файлах миграций в LF (Unix-стиль: \\n)'"
|
||||||
|
aria-label="LF"
|
||||||
|
@click="onApplyMigrationFix('lf')"
|
||||||
|
>
|
||||||
|
{{ language === 'en' ? 'Apply LF Migration Fix' : 'Применить исправление миграции LF' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button
|
||||||
|
:title="language === 'en'
|
||||||
|
? 'Convert all line endings in migration files to CRLF (Windows-style: \\r\\n)'
|
||||||
|
: 'Преобразовать все окончания строк в файлах миграций в CRLF (Windows-стиль: \\r\\n)'"
|
||||||
|
aria-label="CRLF"
|
||||||
|
@click="onApplyMigrationFix('crlf')"
|
||||||
|
>
|
||||||
|
{{ language === 'en' ? 'Apply CRLF Migration Fix' : 'Применить исправление миграции CRLF' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="migrationFixCallbackModel"
|
||||||
|
:header="language === 'en'
|
||||||
|
? '💡 Migration fix report'
|
||||||
|
: '💡 Отчет об исправлении миграции'"
|
||||||
|
:closable="closable">
|
||||||
|
<div class="modal-body">
|
||||||
|
<h2 class="text-lg font-bold text-contrast space-y-2">
|
||||||
|
<template v-if="migrationFixSuccess === true">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
✅
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'The migration fix has been applied successfully. Please restart the launcher and try to log in to the game :)'
|
||||||
|
: 'Исправление миграции успешно применено. Пожалуйста, перезапустите лаунчер и попробуйте снова авторизоваться в игре :)' }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm neon-text">
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'If the problem persists, please try the other fix.'
|
||||||
|
: 'Если проблема сохраняется, пожалуйста, попробуйте другой способ.' }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="migrationFixSuccess === false">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
❌
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'The migration fix failed or had no effect.'
|
||||||
|
: 'Исправление миграции не было успешно применено или не имело эффекта.' }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm neon-text">
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'If the problem persists, please try the other fix.'
|
||||||
|
: 'Если проблема сохраняется, пожалуйста, попробуйте другой способ.' }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
@@ -333,6 +476,9 @@ async function copyToClipboard(text) {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@import '../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
|
||||||
.cta-button {
|
.cta-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { XIcon, PlusIcon } from '@modrinth/assets'
|
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, Checkbox } from '@modrinth/ui'
|
import { Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
GameIcon,
|
GameIcon,
|
||||||
@@ -9,17 +7,20 @@ import {
|
|||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, injectNotificationManager, 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 dayjs from 'dayjs'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { handleError } from '@/store/state.js'
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { finish_install, kill, run } from '@/helpers/profile'
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -93,7 +94,7 @@ const stop = async (e, context) => {
|
|||||||
const repair = async (e) => {
|
const repair = async (e) => {
|
||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
|
|
||||||
await finish_install(props.instance)
|
await finish_install(props.instance).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openFolder = async () => {
|
const openFolder = async () => {
|
||||||
|
|||||||
@@ -163,6 +163,14 @@
|
|||||||
<div v-else class="table-content empty">No profiles found</div>
|
<div v-else class="table-content empty">No profiles found</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
|
<Button
|
||||||
|
v-if="selectedProfileType.name === 'Curseforge'"
|
||||||
|
@click="showCurseForgeProfileModal"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<CodeIcon />
|
||||||
|
Import from Profile Code
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
:disabled="
|
:disabled="
|
||||||
loading ||
|
loading ||
|
||||||
@@ -194,10 +202,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
|
<CurseForgeProfileImportModal ref="curseforgeProfileModal" :close-parent="hide" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import {
|
import {
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
@@ -208,24 +216,29 @@ import {
|
|||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Checkbox, Chips } from '@modrinth/ui'
|
import { Avatar, Button, Checkbox, Chips, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
|
||||||
import { get_loaders } from '@/helpers/tags'
|
|
||||||
import { create } from '@/helpers/profile'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
|
||||||
import {
|
import {
|
||||||
get_default_launcher_path,
|
get_default_launcher_path,
|
||||||
get_importable_instances,
|
get_importable_instances,
|
||||||
import_instance,
|
import_instance,
|
||||||
} from '@/helpers/import.js'
|
} from '@/helpers/import.js'
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
||||||
|
import { create } from '@/helpers/profile'
|
||||||
|
import { get_loaders } from '@/helpers/tags'
|
||||||
|
|
||||||
|
import CurseForgeProfileImportModal from '@/components/ui/CurseForgeProfileImportModal.vue'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const profile_name = ref('')
|
const profile_name = ref('')
|
||||||
const game_version = ref('')
|
const game_version = ref('')
|
||||||
@@ -283,6 +296,11 @@ const hide = () => {
|
|||||||
unlistener.value = null
|
unlistener.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showCurseForgeProfileModal = () => {
|
||||||
|
curseforgeProfileModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (unlistener.value) {
|
if (unlistener.value) {
|
||||||
unlistener.value()
|
unlistener.value()
|
||||||
@@ -305,12 +323,16 @@ const [
|
|||||||
get_game_versions().then(shallowRef).catch(handleError),
|
get_game_versions().then(shallowRef).catch(handleError),
|
||||||
get_loaders()
|
get_loaders()
|
||||||
.then((value) =>
|
.then((value) =>
|
||||||
|
ref(
|
||||||
value
|
value
|
||||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
.filter((item) => item.supported_project_types.includes('modpack'))
|
||||||
.map((item) => item.name.toLowerCase()),
|
.map((item) => item.name.toLowerCase()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.then(ref)
|
.catch((err) => {
|
||||||
.catch(handleError),
|
handleError(err)
|
||||||
|
return ref([])
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
loaders.value.unshift('vanilla')
|
loaders.value.unshift('vanilla')
|
||||||
|
|
||||||
@@ -334,6 +356,7 @@ const game_versions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const modal = ref(null)
|
const modal = ref(null)
|
||||||
|
const curseforgeProfileModal = ref(null)
|
||||||
|
|
||||||
const check_valid = computed(() => {
|
const check_valid = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
|
||||||
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
import { GameIcon, LeftArrowIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
import { Avatar, ButtonStyled } from '@modrinth/ui'
|
||||||
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
type Instance = {
|
type Instance = {
|
||||||
game_version: string
|
game_version: string
|
||||||
|
|||||||
@@ -35,13 +35,15 @@
|
|||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
|
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { find_filtered_jres } from '@/helpers/jre.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { find_filtered_jres } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const chosenInstallOptions = ref([])
|
const chosenInstallOptions = ref([])
|
||||||
const detectJavaModal = ref(null)
|
const detectJavaModal = ref(null)
|
||||||
|
|||||||
@@ -53,20 +53,22 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
SearchIcon,
|
|
||||||
PlayIcon,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
XIcon,
|
|
||||||
FolderSearchIcon,
|
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
FolderSearchIcon,
|
||||||
|
PlayIcon,
|
||||||
|
SearchIcon,
|
||||||
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
version: {
|
version: {
|
||||||
@@ -108,7 +110,6 @@ async function testJava() {
|
|||||||
testingJava.value = true
|
testingJava.value = true
|
||||||
testingJavaSuccess.value = await test_jre(
|
testingJavaSuccess.value = await test_jre(
|
||||||
props.modelValue ? props.modelValue.path : '',
|
props.modelValue ? props.modelValue.path : '',
|
||||||
1,
|
|
||||||
props.version,
|
props.version,
|
||||||
)
|
)
|
||||||
testingJava.value = false
|
testingJava.value = false
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { CheckIcon } from '@modrinth/assets'
|
import { CheckIcon } from '@modrinth/assets'
|
||||||
import { Button, Badge } from '@modrinth/ui'
|
import { Badge, Button } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { update_managed_modrinth_version } from '@/helpers/profile'
|
|
||||||
import { releaseColor } from '@/helpers/utils'
|
|
||||||
import { SwapIcon } from '@/assets/icons/index.js'
|
import { SwapIcon } from '@/assets/icons/index.js'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { update_managed_modrinth_version } from '@/helpers/profile'
|
||||||
|
import { releaseColor } from '@/helpers/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
versions: {
|
versions: {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import type { RouteLocationRaw } from 'vue-router'
|
import type { RouteLocationRaw } from 'vue-router'
|
||||||
import { useRoute, RouterLink } from 'vue-router'
|
import { RouterLink, useRoute } from 'vue-router'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Avatar, TagItem } from '@modrinth/ui'
|
|
||||||
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
import { DownloadIcon, HeartIcon, TagIcon } from '@modrinth/assets'
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
import { Avatar, TagItem } from '@modrinth/ui'
|
||||||
import { computed } from 'vue'
|
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
@@ -21,14 +21,11 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const featuredCategory = computed(() => {
|
const featuredCategory = computed(() => {
|
||||||
if (props.project.categories.includes('optimization')) {
|
if (props.project.display_categories.includes('optimization')) {
|
||||||
return 'optimization'
|
return 'optimization'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.project.categories.length > 0) {
|
return props.project.display_categories[0] ?? props.project.categories[0]
|
||||||
return props.project.categories[0]
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const toColor = computed(() => {
|
const toColor = computed(() => {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { list } from '@/helpers/profile'
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
import { handleError } from '@/store/notifications'
|
import { Avatar, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { onUnmounted, ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
import { Avatar } from '@modrinth/ui'
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { list } from '@/helpers/profile'
|
||||||
import { SpinnerIcon } from '@modrinth/assets'
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const recentInstances = ref([])
|
const recentInstances = ref([])
|
||||||
const getInstances = async () => {
|
const getInstances = async () => {
|
||||||
|
|||||||
@@ -20,12 +20,21 @@
|
|||||||
>
|
>
|
||||||
{{ selectedProcess.profile.name }}
|
{{ selectedProcess.profile.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div v-if="currentProcesses.length > 1" class="arrow button-base" :class="{ rotate: showProfiles }"
|
<div
|
||||||
@click="toggleProfiles()">
|
v-if="currentProcesses.length > 1"
|
||||||
|
class="arrow button-base"
|
||||||
|
:class="{ rotate: showProfiles }"
|
||||||
|
@click="toggleProfiles()"
|
||||||
|
>
|
||||||
<DropdownIcon />
|
<DropdownIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click="stop(selectedProcess)">
|
<Button
|
||||||
|
v-tooltip="'Stop instance'"
|
||||||
|
icon-only
|
||||||
|
class="icon-button stop"
|
||||||
|
@click="stop(selectedProcess)"
|
||||||
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click="goToTerminal()">
|
||||||
@@ -36,44 +45,6 @@
|
|||||||
<span class="circle stopped" />
|
<span class="circle stopped" />
|
||||||
<span class="running-text"> No instances running </span>
|
<span class="running-text"> No instances running </span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="updateState">
|
|
||||||
<a>
|
|
||||||
<Button class="download" :disabled="installState" @click="confirmUpdating(), getRemote(false, false)">
|
|
||||||
<DownloadIcon />
|
|
||||||
{{
|
|
||||||
installState
|
|
||||||
? "Downloading new update..."
|
|
||||||
: "Download new update"
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ModalWrapper ref="confirmUpdate" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="markdown-body">
|
|
||||||
<p>The new version of the AstralRinth launcher is available.</p>
|
|
||||||
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
|
||||||
<p><strong>⚠️ Warning ⚠️</strong></p>
|
|
||||||
<p>
|
|
||||||
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
|
|
||||||
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
|
|
||||||
your files, so you should always make copies of them and keep them in a safe place.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span>Source • Git Astralium</span>
|
|
||||||
<span>Version on remote server • <p id="releaseData" class="cosmic inline-fix"></p></span>
|
|
||||||
<span>Version on local device •
|
|
||||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
|
||||||
</span>
|
|
||||||
<div class="button-group push-right">
|
|
||||||
<Button class="download-modal" @click="confirmUpdate.hide()">
|
|
||||||
Decline</Button>
|
|
||||||
<Button class="download-modal" @click="approveUpdate()">
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</div>
|
</div>
|
||||||
<transition name="download">
|
<transition name="download">
|
||||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
||||||
@@ -83,20 +54,39 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
<ProgressBar :progress="Math.floor((100 * loadingBar.current) / loadingBar.total)" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}% {{ loadingBar.message }}
|
{{ Math.floor((100 * loadingBar.current) / loadingBar.total) }}%
|
||||||
|
{{ loadingBar.message }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="download">
|
<transition name="download">
|
||||||
<Card v-if="showProfiles === true && currentProcesses.length > 0" ref="profiles" class="profile-card">
|
<Card
|
||||||
<Button v-for="process in currentProcesses" :key="process.uuid" class="profile-button"
|
v-if="showProfiles === true && currentProcesses.length > 0"
|
||||||
@click="selectProcess(process)">
|
ref="profiles"
|
||||||
|
class="profile-card"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-for="process in currentProcesses"
|
||||||
|
:key="process.uuid"
|
||||||
|
class="profile-button"
|
||||||
|
@click="selectProcess(process)"
|
||||||
|
>
|
||||||
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
<div class="text"><span class="circle running" /> {{ process.profile.name }}</div>
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop" @click.stop="stop(process)">
|
<Button
|
||||||
|
v-tooltip="'Stop instance'"
|
||||||
|
icon-only
|
||||||
|
class="icon-button stop"
|
||||||
|
@click.stop="stop(process)"
|
||||||
|
>
|
||||||
<StopCircleIcon />
|
<StopCircleIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button" @click.stop="goToTerminal(process.profile.path)">
|
<Button
|
||||||
|
v-tooltip="'View logs'"
|
||||||
|
icon-only
|
||||||
|
class="icon-button"
|
||||||
|
@click.stop="goToTerminal(process.profile.path)"
|
||||||
|
>
|
||||||
<TerminalSquareIcon />
|
<TerminalSquareIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -107,40 +97,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
DropdownIcon,
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TerminalSquareIcon,
|
TerminalSquareIcon,
|
||||||
DropdownIcon,
|
|
||||||
UnplugIcon,
|
UnplugIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, ButtonStyled, Card } from '@modrinth/ui'
|
import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
|
||||||
import { loading_listener, process_listener } from '@/helpers/events'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { progress_bars_list } from '@/helpers/state.js'
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { get_many } from '@/helpers/profile.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { loading_listener, process_listener } from '@/helpers/events'
|
||||||
|
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||||
|
import { get_many } from '@/helpers/profile.js'
|
||||||
|
import { progress_bars_list } from '@/helpers/state.js'
|
||||||
|
|
||||||
const version = await getVersion()
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
|
||||||
import ModalWrapper from './modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
const confirmUpdate = ref(null)
|
|
||||||
|
|
||||||
const confirmUpdating = async () => {
|
|
||||||
confirmUpdate.value.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const approveUpdate = async () => {
|
|
||||||
confirmUpdate.value.hide()
|
|
||||||
await getRemote(true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
await getRemote(true, false)
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const card = ref(null)
|
const card = ref(null)
|
||||||
@@ -298,101 +271,6 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.inline-fix {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-top: -2rem;
|
|
||||||
margin-bottom: -2rem;
|
|
||||||
//margin-left: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cosmic {
|
|
||||||
color: #3e8cde;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
:deep(table) {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(hr),
|
|
||||||
:deep(h1),
|
|
||||||
:deep(h2) {
|
|
||||||
max-width: max(60rem, 90%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(ul),
|
|
||||||
:deep(ol) {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: var(--gap-lg);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download {
|
|
||||||
color: #3e8cde;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
// padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download:hover,
|
|
||||||
.download:focus,
|
|
||||||
.download:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-modal {
|
|
||||||
color: #3e8cde;
|
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-modal:hover,
|
|
||||||
.download-modal:focus,
|
|
||||||
.download-modal:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-groups {
|
.action-groups {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -404,7 +282,6 @@ onBeforeUnmount(() => {
|
|||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&.rotate {
|
&.rotate {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
@@ -426,10 +303,8 @@ onBeforeUnmount(() => {
|
|||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none; /* Safari */
|
||||||
/* Safari */
|
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||||
-ms-user-select: none;
|
|
||||||
/* IE 10 and IE 11 */
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&.clickable:hover {
|
&.clickable:hover {
|
||||||
|
|||||||
@@ -117,16 +117,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { TagsIcon, DownloadIcon, HeartIcon, PlusIcon, CheckIcon } from '@modrinth/assets'
|
import { CheckIcon, DownloadIcon, HeartIcon, PlusIcon, TagsIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, Avatar } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { formatNumber, formatCategory } from '@modrinth/utils'
|
import { formatCategory, formatNumber } from '@modrinth/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { ref, computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -174,7 +177,7 @@ async function install() {
|
|||||||
(profile) => {
|
(profile) => {
|
||||||
router.push(`/instance/${profile}`)
|
router.push(`/instance/${profile}`)
|
||||||
},
|
},
|
||||||
)
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const modpack = computed(() => props.project.project_type === 'modpack')
|
const modpack = computed(() => props.project.project_type === 'modpack')
|
||||||
|
|||||||
@@ -82,11 +82,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { MaximizeIcon, MinimizeIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
import { loading_listener } from '@/helpers/events.js'
|
import { loading_listener } from '@/helpers/events.js'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
||||||
import { XIcon, MaximizeIcon, MinimizeIcon } from '@modrinth/assets'
|
|
||||||
import { getOS } from '@/helpers/utils.js'
|
import { getOS } from '@/helpers/utils.js'
|
||||||
import { useLoading } from '@/store/loading.js'
|
import { useLoading } from '@/store/loading.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
|
||||||
import { get_categories } from '@/helpers/tags.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { get_version, get_project } from '@/helpers/cache.js'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import SearchCard from '@/components/ui/SearchCard.vue'
|
||||||
|
import { get_project, get_version } from '@/helpers/cache.js'
|
||||||
|
import { get_categories } from '@/helpers/tags.js'
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
const project = ref(null)
|
const project = ref(null)
|
||||||
@@ -37,7 +39,14 @@ defineExpose({
|
|||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
confirmModal.value.hide()
|
confirmModal.value.hide()
|
||||||
await installVersion(project.value.id, version.value.id, null, 'URLConfirmModal')
|
await installVersion(
|
||||||
|
project.value.id,
|
||||||
|
version.value.id,
|
||||||
|
null,
|
||||||
|
'URLConfirmModal',
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
).catch(handleError)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
|
||||||
import {
|
import {
|
||||||
UserPlusIcon,
|
|
||||||
MoreVerticalIcon,
|
|
||||||
MailIcon,
|
MailIcon,
|
||||||
|
MoreVerticalIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
UserPlusIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { ref, onUnmounted, watch, computed } from 'vue'
|
import {
|
||||||
import { friend_listener } from '@/helpers/events'
|
Avatar,
|
||||||
import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
|
ButtonStyled,
|
||||||
import { get_user_many } from '@/helpers/cache'
|
injectNotificationManager,
|
||||||
import { handleError } from '@/store/notifications.js'
|
OverflowMenu,
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
useRelativeTime,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { get_user_many } from '@/helpers/cache'
|
||||||
|
import { friend_listener } from '@/helpers/events'
|
||||||
|
import { add_friend, friend_statuses, friends, remove_friend } from '@/helpers/friends'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -56,16 +56,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { Button } from '@modrinth/ui'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
import { formatCategory } from '@modrinth/utils'
|
||||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const instance = ref(null)
|
const instance = ref(null)
|
||||||
const project = ref(null)
|
const project = ref(null)
|
||||||
const versions = ref(null)
|
const versions = ref(null)
|
||||||
@@ -76,10 +78,10 @@ const installing = ref(false)
|
|||||||
const onInstall = ref(() => {})
|
const onInstall = ref(() => {})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (instanceVal, projectVal, projectVersions, callback) => {
|
show: (instanceVal, projectVal, projectVersions, selected, callback) => {
|
||||||
instance.value = instanceVal
|
instance.value = instanceVal
|
||||||
versions.value = projectVersions
|
versions.value = projectVersions
|
||||||
selectedVersion.value = projectVersions[0]
|
selectedVersion.value = selected ?? projectVersions[0]
|
||||||
|
|
||||||
project.value = projectVal
|
project.value = projectVal
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button } from '@modrinth/ui'
|
import { Button, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const versionId = ref()
|
const versionId = ref()
|
||||||
const project = ref()
|
const project = ref()
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
|
CheckIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
RightArrowIcon,
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
RightArrowIcon,
|
|
||||||
CheckIcon,
|
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import {
|
import {
|
||||||
add_project_from_version as installMod,
|
add_project_from_version as installMod,
|
||||||
check_installed,
|
check_installed,
|
||||||
|
create,
|
||||||
get,
|
get,
|
||||||
list,
|
list,
|
||||||
create,
|
|
||||||
} from '@/helpers/profile'
|
} from '@/helpers/profile'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { installVersionDependencies } from '@/store/install.js'
|
import { installVersionDependencies } from '@/store/install.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const versions = ref()
|
const versions = ref()
|
||||||
@@ -109,7 +110,7 @@ async function install(instance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await installMod(instance.path, version.id).catch(handleError)
|
await installMod(instance.path, version.id).catch(handleError)
|
||||||
await installVersionDependencies(instance, version)
|
await installVersionDependencies(instance, version).catch(handleError)
|
||||||
|
|
||||||
instance.installedMod = true
|
instance.installedMod = true
|
||||||
instance.installing = false
|
instance.installing = false
|
||||||
@@ -184,7 +185,7 @@ const createInstance = async () => {
|
|||||||
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
await router.push(`/instance/${encodeURIComponent(id)}/`)
|
||||||
|
|
||||||
const instance = await get(id, true)
|
const instance = await get(id, true)
|
||||||
await installVersionDependencies(instance, versions.value[0])
|
await installVersionDependencies(instance, versions.value[0]).catch(handleError)
|
||||||
|
|
||||||
trackEvent('InstanceCreate', {
|
trackEvent('InstanceCreate', {
|
||||||
profile_name: name.value,
|
profile_name: name.value,
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
Checkbox,
|
||||||
|
injectNotificationManager,
|
||||||
|
OverflowMenu,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
|
|
||||||
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
|
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
|
||||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||||
|
|
||||||
|
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox } from '@modrinth/ui'
|
import { Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { edit } from '@/helpers/profile'
|
|
||||||
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
TransferIcon,
|
|
||||||
IssuesIcon,
|
|
||||||
HammerIcon,
|
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
WrenchIcon,
|
HammerIcon,
|
||||||
UndoIcon,
|
IssuesIcon,
|
||||||
SpinnerIcon,
|
SpinnerIcon,
|
||||||
UnplugIcon,
|
TransferIcon,
|
||||||
|
UndoIcon,
|
||||||
UnlinkIcon,
|
UnlinkIcon,
|
||||||
|
UnplugIcon,
|
||||||
|
WrenchIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Checkbox, Chips, ButtonStyled, TeleportDropdownMenu } from '@modrinth/ui'
|
import {
|
||||||
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
Avatar,
|
||||||
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
ButtonStyled,
|
||||||
import { handleError } from '@/store/notifications'
|
Checkbox,
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
Chips,
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
injectNotificationManager,
|
||||||
import { get_loader_versions } from '@/helpers/metadata'
|
TeleportDropdownMenu,
|
||||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
} from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
formatCategory,
|
formatCategory,
|
||||||
type GameVersionTag,
|
type GameVersionTag,
|
||||||
@@ -25,16 +25,31 @@ import {
|
|||||||
type Project,
|
type Project,
|
||||||
type Version,
|
type Version,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_project, get_version_many } from '@/helpers/cache'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_project, get_version_many } from '@/helpers/cache'
|
||||||
|
import { get_loader_versions } from '@/helpers/metadata'
|
||||||
|
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
||||||
|
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
InstanceSettingsTabProps,
|
InstanceSettingsTabProps,
|
||||||
ManifestLoaderVersion,
|
|
||||||
Manifest,
|
Manifest,
|
||||||
|
ManifestLoaderVersion,
|
||||||
} from '../../../helpers/types'
|
} from '../../../helpers/types'
|
||||||
|
|
||||||
|
import { initAuthlibPatching } from '@/helpers/utils.js'
|
||||||
|
const authLibPatchingModal = ref(null)
|
||||||
|
const isAuthLibPatchedSuccess = ref(false)
|
||||||
|
const isAuthLibPatching = ref(false)
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const repairConfirmModal = ref()
|
const repairConfirmModal = ref()
|
||||||
@@ -447,9 +462,43 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'reinstall',
|
defaultMessage: 'reinstall',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function handleInitAuthLibPatching(ismojang: boolean) {
|
||||||
|
isAuthLibPatching.value = true
|
||||||
|
let state = false
|
||||||
|
let instance_path = props.instance.loader_version != null ? props.instance.game_version + "-" + props.instance.loader_version : props.instance.game_version
|
||||||
|
try {
|
||||||
|
state = await initAuthlibPatching(instance_path, ismojang)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
isAuthLibPatching.value = false
|
||||||
|
isAuthLibPatchedSuccess.value = state
|
||||||
|
authLibPatchingModal.value.show()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="authLibPatchingModal"
|
||||||
|
:header="'AuthLib installation report'"
|
||||||
|
:closable="true"
|
||||||
|
@close="authLibPatchingModal.hide()"
|
||||||
|
>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h2 class="text-lg font-bold text-contrast space-y-2">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
<span v-if="isAuthLibPatchedSuccess" class="neon-text">
|
||||||
|
AuthLib installation completed successfully! Now you can log in and play!
|
||||||
|
</span>
|
||||||
|
<span v-else class="neon-text">
|
||||||
|
Failed to install AuthLib. It's possible that no compatible AuthLib version was found for the selected game and/or mod loader version.
|
||||||
|
There may also be a problem with accessing resources behind CloudFlare.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="repairConfirmModal"
|
ref="repairConfirmModal"
|
||||||
:title="formatMessage(messages.repairConfirmTitle)"
|
:title="formatMessage(messages.repairConfirmTitle)"
|
||||||
@@ -505,7 +554,8 @@ const messages = defineMessages({
|
|||||||
</div>
|
</div>
|
||||||
<div v-else-if="!modpackProject && instance.linked_data && !fetching" class="mb-2">
|
<div v-else-if="!modpackProject && instance.linked_data && !fetching" class="mb-2">
|
||||||
<p class="text-brand-red font-medium mt-0">
|
<p class="text-brand-red font-medium mt-0">
|
||||||
<IssuesIcon class="top-[3px] relative" /> {{ formatMessage(messages.noModpackFound) }}
|
<IssuesIcon class="top-[3px] relative" />
|
||||||
|
{{ formatMessage(messages.noModpackFound) }}
|
||||||
</p>
|
</p>
|
||||||
<p>{{ formatMessage(messages.debugInformation) }}</p>
|
<p>{{ formatMessage(messages.debugInformation) }}</p>
|
||||||
<div class="bg-bg p-6 rounded-2xl mt-2 text-sm text-secondary">
|
<div class="bg-bg p-6 rounded-2xl mt-2 text-sm text-secondary">
|
||||||
@@ -532,7 +582,9 @@ const messages = defineMessages({
|
|||||||
{{
|
{{
|
||||||
modpackProject
|
modpackProject
|
||||||
? modpackProject.title
|
? modpackProject.title
|
||||||
: formatMessage(messages.minecraftVersion, { version: instance.game_version })
|
: formatMessage(messages.minecraftVersion, {
|
||||||
|
version: instance.game_version,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm text-secondary leading-none">
|
<span class="text-sm text-secondary leading-none">
|
||||||
@@ -662,7 +714,12 @@ const messages = defineMessages({
|
|||||||
/>
|
/>
|
||||||
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
|
<div v-else class="mt-2 text-brand-red flex gap-2 items-center">
|
||||||
<IssuesIcon />
|
<IssuesIcon />
|
||||||
{{ formatMessage(messages.noLoaderVersions, { loader: loader, version: gameVersion }) }}
|
{{
|
||||||
|
formatMessage(messages.noLoaderVersions, {
|
||||||
|
loader: loader,
|
||||||
|
version: gameVersion,
|
||||||
|
})
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
@@ -720,6 +777,24 @@ const messages = defineMessages({
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||||
|
<div v-if="isAuthLibPatching" class="w-6 h-6 cursor-pointer hover:brightness-75 neon-icon pulse">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
Auth system (Skins) <span class="text-sm font-bold px-2 bg-brand-highlight text-brand rounded-full">Beta</span>
|
||||||
|
</h2>
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button :disabled="isAuthLibPatching" @click="handleInitAuthLibPatching(true)">
|
||||||
|
Install Microsoft
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button :disabled="isAuthLibPatching" @click="handleInitAuthLibPatching(false) ">
|
||||||
|
Install Ely.By
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="instance.linked_data && instance.linked_data.locked">
|
<template v-if="instance.linked_data && instance.linked_data.locked">
|
||||||
@@ -787,3 +862,9 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox, Slider } from '@modrinth/ui'
|
|
||||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||||
import { computed, readonly, ref, watch } from 'vue'
|
import { Checkbox, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
|
||||||
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 { computed, readonly, ref, watch } from 'vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
|
||||||
|
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
import type { AppSettings, InstanceSettingsTabProps, MemorySettings } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|
||||||
const globalSettings = (await get().catch(handleError)) as AppSettings
|
const globalSettings = (await get().catch(handleError)) as unknown as AppSettings
|
||||||
|
|
||||||
const overrideJavaInstall = ref(!!props.instance.java_path)
|
const overrideJavaInstall = ref(!!props.instance.java_path)
|
||||||
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
const optimalJava = readonly(await get_optimal_jre_key(props.instance.path).catch(handleError))
|
||||||
@@ -34,7 +36,10 @@ const envVars = ref(
|
|||||||
|
|
||||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
||||||
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||||
|
maxMemory: number
|
||||||
|
snapPoints: number[]
|
||||||
|
}
|
||||||
|
|
||||||
const editProfileObject = computed(() => {
|
const editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
@@ -156,6 +161,8 @@ const messages = defineMessages({
|
|||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
|
:snap-points="snapPoints"
|
||||||
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Checkbox, Toggle } from '@modrinth/ui'
|
import { Checkbox, injectNotificationManager, Toggle } from '@modrinth/ui'
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { computed, type Ref, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { edit } from '@/helpers/profile'
|
import { edit } from '@/helpers/profile'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|||||||
@@ -1,28 +1,48 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ReportIcon,
|
|
||||||
AstralRinthLogo,
|
|
||||||
ShieldIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
GaugeIcon,
|
|
||||||
PaintbrushIcon,
|
|
||||||
GameIcon,
|
|
||||||
CoffeeIcon,
|
CoffeeIcon,
|
||||||
|
GameIcon,
|
||||||
|
GaugeIcon,
|
||||||
|
AstralRinthLogo,
|
||||||
|
DownloadIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
PaintbrushIcon,
|
||||||
|
ReportIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
ShieldIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { TabbedModal } from '@modrinth/ui'
|
import { TabbedModal } from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { useVIntl, defineMessage } from '@vintl/vintl'
|
|
||||||
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
|
||||||
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
|
||||||
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
|
||||||
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
|
||||||
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { version as getOsVersion, platform as getOsPlatform } from '@tauri-apps/plugin-os'
|
import { platform as getOsPlatform, version as getOsVersion } from '@tauri-apps/plugin-os'
|
||||||
import { useTheming } from '@/store/state'
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import AppearanceSettings from '@/components/ui/settings/AppearanceSettings.vue'
|
||||||
|
import DefaultInstanceSettings from '@/components/ui/settings/DefaultInstanceSettings.vue'
|
||||||
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
|
import JavaSettings from '@/components/ui/settings/JavaSettings.vue'
|
||||||
|
import PrivacySettings from '@/components/ui/settings/PrivacySettings.vue'
|
||||||
|
import ResourceManagementSettings from '@/components/ui/settings/ResourceManagementSettings.vue'
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
// [AR] Imports
|
||||||
|
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
||||||
|
|
||||||
|
const updateModalView = ref(null)
|
||||||
|
const updateRequestFailView = ref(null)
|
||||||
|
|
||||||
|
const initUpdateModal = async () => {
|
||||||
|
updateModalView.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const initDownload = async () => {
|
||||||
|
updateModalView.value.hide()
|
||||||
|
const result = await getRemote(true);
|
||||||
|
if (!result) {
|
||||||
|
updateRequestFailView.value.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -140,7 +160,10 @@ function devModeCount() {
|
|||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
:class="{
|
||||||
|
'text-brand': themeStore.devMode,
|
||||||
|
'text-secondary': !themeStore.devMode,
|
||||||
|
}"
|
||||||
@click="devModeCount"
|
@click="devModeCount"
|
||||||
>
|
>
|
||||||
<AstralRinthLogo class="w-6 h-6" />
|
<AstralRinthLogo class="w-6 h-6" />
|
||||||
@@ -152,10 +175,81 @@ function devModeCount() {
|
|||||||
<span v-else class="capitalize">{{ osPlatform }}</span>
|
<span v-else class="capitalize">{{ osPlatform }}</span>
|
||||||
{{ osVersion }}
|
{{ osVersion }}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="updateState" class="w-8 h-8 cursor-pointer hover:brightness-75 neon-icon pulse">
|
||||||
|
<template v-if="installState">
|
||||||
|
<SpinnerIcon class="size-6 animate-spin" v-tooltip.bottom="'Installing in process...'" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DownloadIcon class="size-6" v-tooltip.bottom="'View update info'" @click="!installState && (initUpdateModal(), getRemote(false))" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</TabbedModal>
|
</TabbedModal>
|
||||||
|
<!-- [AR] Feature -->
|
||||||
|
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>The new version of the AstralRinth launcher is available.</p>
|
||||||
|
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
||||||
|
<p><strong>⚠️ Warning ⚠️</strong></p>
|
||||||
|
<p>
|
||||||
|
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
|
||||||
|
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
|
||||||
|
your files, so you should always make copies of them and keep them in a safe place.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-secondary space-y-1">
|
||||||
|
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank"
|
||||||
|
rel="noopener noreferrer"><strong>Source:</strong> Git Astralium</a>
|
||||||
|
<p>
|
||||||
|
<strong>Version on remote server:</strong>
|
||||||
|
<span id="releaseData" class="neon-text"></span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Version on local device:</strong>
|
||||||
|
<span class="neon-text">v{{ version }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateModalView.hide()">Cancel</Button>
|
||||||
|
<Button class="bordered" @click="initDownload()">Download file</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="updateRequestFailView" :has-to-type="false" header="Failed to request a file from the server :(">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p><strong>Error occurred</strong></p>
|
||||||
|
<p>Unfortunately, the program was unable to download the file from our servers.</p>
|
||||||
|
<p>
|
||||||
|
Please try downloading it yourself from
|
||||||
|
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">Git
|
||||||
|
Astralium</a>
|
||||||
|
if there are any updates available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-secondary">
|
||||||
|
<p>
|
||||||
|
<strong>Local AstralRinth:</strong>
|
||||||
|
<span class="neon-text">v{{ version }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateRequestFailView.hide()">Close</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
onFlowCancel: {
|
||||||
|
type: Function,
|
||||||
|
default() {
|
||||||
|
return async () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const modal = ref()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
modal.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
modal.value.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ show, hide })
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
||||||
|
<template #title>
|
||||||
|
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
||||||
|
<LogInIcon /> Sign in
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex justify-center gap-2">
|
||||||
|
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-secondary">
|
||||||
|
Please sign in at the browser window that just opened to continue.
|
||||||
|
</p>
|
||||||
|
</ModalWrapper>
|
||||||
|
</template>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ConfirmModal } from '@modrinth/ui'
|
import { ConfirmModal } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import { useTheming } from '@/store/theme.js'
|
// import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -52,10 +53,11 @@ const modal = ref(null)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: () => {
|
show: () => {
|
||||||
|
// hide_ads_window()
|
||||||
modal.value.show()
|
modal.value.show()
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
// onModalHide()
|
onModalHide()
|
||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ChevronRightIcon } from '@modrinth/assets'
|
import { ChevronRightIcon } from '@modrinth/assets'
|
||||||
import { Avatar } from '@modrinth/ui'
|
import { Avatar } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
|
CodeIcon,
|
||||||
CoffeeIcon,
|
CoffeeIcon,
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
WrenchIcon,
|
|
||||||
MonitorIcon,
|
MonitorIcon,
|
||||||
CodeIcon,
|
WrenchIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
import { Avatar, TabbedModal, type TabbedModalTab } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import GeneralSettings from '@/components/ui/instance_settings/GeneralSettings.vue'
|
||||||
|
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
||||||
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
import InstallationSettings from '@/components/ui/instance_settings/InstallationSettings.vue'
|
||||||
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
import JavaSettings from '@/components/ui/instance_settings/JavaSettings.vue'
|
||||||
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
import WindowSettings from '@/components/ui/instance_settings/WindowSettings.vue'
|
||||||
import HooksSettings from '@/components/ui/instance_settings/HooksSettings.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef } 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 { useTemplateRef } from 'vue'
|
||||||
import { useTheming } from '@/store/theme.js'
|
|
||||||
|
// import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ const props = defineProps({
|
|||||||
onHide: {
|
onHide: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default() {
|
default() {
|
||||||
return () => { }
|
return () => {}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// showAdOnClose: {
|
// showAdOnClose: {
|
||||||
@@ -40,9 +41,9 @@ defineExpose({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function onModalHide() {
|
function onModalHide() {
|
||||||
// if (props.showAdOnClose) {
|
// if (props.showAdOnClose) {
|
||||||
// show_ads_window()
|
// show_ads_window()
|
||||||
// }
|
// }
|
||||||
props.onHide?.()
|
props.onHide?.()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ShareModal } from '@modrinth/ui'
|
import { ShareModal } from '@modrinth/ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
import { useTheming } from '@/store/theme.js'
|
// import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ const modal = ref(null)
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show: (passedContent) => {
|
show: (passedContent) => {
|
||||||
|
// hide_ads_window()
|
||||||
modal.value.show(passedContent)
|
modal.value.show(passedContent)
|
||||||
},
|
},
|
||||||
hide: () => {
|
hide: () => {
|
||||||
@@ -40,9 +42,21 @@ defineExpose({
|
|||||||
modal.value.hide()
|
modal.value.hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// function onModalHide() {
|
||||||
|
// show_ads_window()
|
||||||
|
// }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ShareModal ref="modal" :header="header" :share-title="shareTitle" :share-text="shareText" :link="link"
|
<ShareModal
|
||||||
:open-in-new-tab="openInNewTab" :on-hide="onModalHide" :noblur="!themeStore.advancedRendering" />
|
ref="modal"
|
||||||
|
:header="header"
|
||||||
|
:share-title="shareTitle"
|
||||||
|
:share-text="shareText"
|
||||||
|
:link="link"
|
||||||
|
:open-in-new-tab="openInNewTab"
|
||||||
|
:on-hide="onModalHide"
|
||||||
|
:noblur="!themeStore.advancedRendering"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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 { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { getOS } from '@/helpers/utils'
|
import { getOS } from '@/helpers/utils'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
import type { ColorTheme } from '@/store/theme.ts'
|
import type { ColorTheme } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { injectNotificationManager, Slider, Toggle } from '@modrinth/ui'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
|
||||||
import { handleError } from '@/store/notifications'
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
import { Slider, Toggle } from '@modrinth/ui'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const fetchSettings = await get()
|
const fetchSettings = await get()
|
||||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||||
@@ -11,7 +13,10 @@ fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).jo
|
|||||||
|
|
||||||
const settings = ref(fetchSettings)
|
const settings = ref(fetchSettings)
|
||||||
|
|
||||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
const { maxMemory, snapPoints } = (await useMemorySlider().catch(handleError)) as unknown as {
|
||||||
|
maxMemory: number
|
||||||
|
snapPoints: number[]
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
@@ -107,6 +112,8 @@ watch(
|
|||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
|
:snap-points="snapPoints"
|
||||||
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/state'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||||
|
import { useTheming } from '@/store/state'
|
||||||
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
import { DEFAULT_FEATURE_FLAGS, type FeatureFlag } from '@/store/theme.ts'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||||
async function updateJavaVersion(version) {
|
async function updateJavaVersion(version) {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { Toggle } from '@modrinth/ui'
|
import { Toggle } from '@modrinth/ui'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
import { optInAnalytics, optOutAnalytics } from '@/helpers/analytics'
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
@@ -26,11 +27,11 @@ watch(
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Personalized ads</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
(Hard disabled by AR) • Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
Modrinth's ad provider, Aditude, shows ads based on your preferences. By disabling this
|
||||||
option, you opt out and ads will no longer be shown based on your interests.
|
option, you opt out and ads will no longer be shown based on your interests.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- AstralRinth disabled element by default -->
|
<!-- [AR] Patch. Disabled element by default -->
|
||||||
<Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" />
|
<Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -38,12 +39,12 @@ watch(
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
<h2 class="m-0 text-lg font-extrabold text-contrast">Telemetry</h2>
|
||||||
<p class="m-0 text-sm">
|
<p class="m-0 text-sm">
|
||||||
(Hard disabled by AR) • Modrinth collects anonymized analytics and usage data to improve our user experience and
|
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
||||||
customize your experience. By disabling this option, you opt out and your data will no
|
customize your experience. By disabling this option, you opt out and your data will no
|
||||||
longer be collected.
|
longer be collected.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- AstralRinth disabled element by default -->
|
<!-- [AR] Patch. Disabled element by default -->
|
||||||
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
|
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Slider } from '@modrinth/ui'
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { purge_cache_types } from '@/helpers/cache.js'
|
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { Button, injectNotificationManager, Slider } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { purge_cache_types } from '@/helpers/cache.js'
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -100,36 +100,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, useTemplateRef } from 'vue'
|
|
||||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
|
||||||
import {
|
import {
|
||||||
SkinPreviewRenderer,
|
CheckIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
SaveIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
UploadIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
Button,
|
Button,
|
||||||
RadioButtons,
|
ButtonStyled,
|
||||||
CapeButton,
|
CapeButton,
|
||||||
CapeLikeTextButton,
|
CapeLikeTextButton,
|
||||||
ButtonStyled,
|
injectNotificationManager,
|
||||||
|
RadioButtons,
|
||||||
|
SkinPreviewRenderer,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||||
|
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||||
import {
|
import {
|
||||||
add_and_equip_custom_skin,
|
add_and_equip_custom_skin,
|
||||||
remove_custom_skin,
|
|
||||||
unequip_skin,
|
|
||||||
type Skin,
|
|
||||||
type Cape,
|
type Cape,
|
||||||
type SkinModel,
|
determineModelType,
|
||||||
get_normalized_skin_texture,
|
get_normalized_skin_texture,
|
||||||
|
remove_custom_skin,
|
||||||
|
type Skin,
|
||||||
|
type SkinModel,
|
||||||
|
unequip_skin,
|
||||||
} from '@/helpers/skins.ts'
|
} from '@/helpers/skins.ts'
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import {
|
const { handleError } = injectNotificationManager()
|
||||||
UploadIcon,
|
|
||||||
CheckIcon,
|
|
||||||
SaveIcon,
|
|
||||||
XIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
|
||||||
|
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||||
@@ -253,7 +257,7 @@ async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
|||||||
mode.value = 'new'
|
mode.value = 'new'
|
||||||
currentSkin.value = null
|
currentSkin.value = null
|
||||||
uploadedTextureUrl.value = skinTextureUrl
|
uploadedTextureUrl.value = skinTextureUrl
|
||||||
variant.value = 'CLASSIC'
|
variant.value = await determineModelType(skinTextureUrl)
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTemplateRef, ref, computed } from 'vue'
|
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||||
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
|
||||||
import {
|
import {
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
ScrollablePanel,
|
|
||||||
CapeButton,
|
CapeButton,
|
||||||
CapeLikeTextButton,
|
CapeLikeTextButton,
|
||||||
|
ScrollablePanel,
|
||||||
SkinPreviewRenderer,
|
SkinPreviewRenderer,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
import { computed, ref, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import type { Cape, SkinModel } from '@/helpers/skins.ts'
|
||||||
|
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
|
|
||||||
|
|||||||
@@ -27,14 +27,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onBeforeUnmount, watch } from 'vue'
|
|
||||||
import { UploadIcon } from '@modrinth/assets'
|
import { UploadIcon } from '@modrinth/assets'
|
||||||
import { useNotifications } from '@/store/state'
|
import { injectNotificationManager } from '@modrinth/ui'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
|
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_dragged_skin_data } from '@/helpers/skins'
|
import { get_dragged_skin_data } from '@/helpers/skins'
|
||||||
|
|
||||||
const notifications = useNotifications()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
const fileInput = ref<HTMLInputElement>()
|
const fileInput = ref<HTMLInputElement>()
|
||||||
@@ -99,7 +100,7 @@ async function setupDragDropListener() {
|
|||||||
const data = await get_dragged_skin_data(filePath)
|
const data = await get_dragged_skin_data(filePath)
|
||||||
await processData(data.buffer)
|
await processData(data.buffer)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.addNotification({
|
addNotification({
|
||||||
title: 'Error processing file',
|
title: 'Error processing file',
|
||||||
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Dayjs } from 'dayjs'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import {
|
import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
@@ -13,25 +11,29 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
|
injectNotificationManager,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
SmartClickable,
|
SmartClickable,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useVIntl } from '@vintl/vintl'
|
|
||||||
import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import { get_project } from '@/helpers/cache'
|
|
||||||
import { capitalizeString } from '@modrinth/utils'
|
import { capitalizeString } from '@modrinth/utils'
|
||||||
import { kill, run } from '@/helpers/profile'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { handleSevereError } from '@/store/error'
|
import { useVIntl } from '@vintl/vintl'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import type { Dayjs } from 'dayjs'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
import dayjs from 'dayjs'
|
||||||
import { handleError } from '@/store/notifications'
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_project } from '@/helpers/cache'
|
||||||
|
import { process_listener } from '@/helpers/events'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { kill, run } from '@/helpers/profile'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils'
|
||||||
|
import { handleSevereError } from '@/store/error'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
||||||
type ServerWorld,
|
|
||||||
type ServerData,
|
|
||||||
type WorldWithProfile,
|
|
||||||
get_recent_worlds,
|
|
||||||
getWorldIdentifier,
|
|
||||||
get_profile_protocol_version,
|
|
||||||
refreshServerData,
|
|
||||||
start_join_server,
|
|
||||||
start_join_singleplayer_world,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
|
||||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
|
||||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
|
||||||
import { watch, onMounted, onUnmounted, ref, 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.ts'
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { kill, run } from '@/helpers/profile'
|
|
||||||
import { handleError } from '@/store/notifications'
|
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||||
|
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||||
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 { kill, run } from '@/helpers/profile'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import {
|
||||||
|
get_profile_protocol_version,
|
||||||
|
get_recent_worlds,
|
||||||
|
getWorldIdentifier,
|
||||||
|
type ProtocolVersion,
|
||||||
|
refreshServerData,
|
||||||
|
type ServerData,
|
||||||
|
type ServerWorld,
|
||||||
|
start_join_server,
|
||||||
|
start_join_singleplayer_world,
|
||||||
|
type WorldWithProfile,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
import { handleSevereError } from '@/store/error'
|
import { handleSevereError } from '@/store/error'
|
||||||
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
recentInstances: GameInstance[]
|
recentInstances: GameInstance[]
|
||||||
@@ -33,7 +36,7 @@ const theme = useTheming()
|
|||||||
|
|
||||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||||
const serverData = ref<Record<string, ServerData>>({})
|
const serverData = ref<Record<string, ServerData>>({})
|
||||||
const protocolVersions = ref<Record<string, number | null>>({})
|
const protocolVersions = ref<Record<string, ProtocolVersion | null>>({})
|
||||||
|
|
||||||
const MIN_JUMP_BACK_IN = 3
|
const MIN_JUMP_BACK_IN = 3
|
||||||
const MAX_JUMP_BACK_IN = 6
|
const MAX_JUMP_BACK_IN = 6
|
||||||
@@ -121,11 +124,8 @@ async function populateJumpBackIn() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// fetch each server's data
|
servers.forEach(({ instancePath, address }) =>
|
||||||
Promise.all(
|
|
||||||
servers.map(({ instancePath, address }) =>
|
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +150,8 @@ async function populateJumpBackIn() {
|
|||||||
.slice(0, MAX_JUMP_BACK_IN)
|
.slice(0, MAX_JUMP_BACK_IN)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshServer(address: string, instancePath: string) {
|
function refreshServer(address: string, instancePath: string) {
|
||||||
await refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinWorld(world: WorldWithProfile) {
|
async function joinWorld(world: WorldWithProfile) {
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
|
||||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
|
||||||
import {
|
import {
|
||||||
useRelativeTime,
|
|
||||||
Avatar,
|
|
||||||
ButtonStyled,
|
|
||||||
commonMessages,
|
|
||||||
OverflowMenu,
|
|
||||||
SmartClickable,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import {
|
|
||||||
IssuesIcon,
|
|
||||||
EyeIcon,
|
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
|
IssuesIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
NoSignalIcon,
|
NoSignalIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
@@ -29,14 +17,33 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
ButtonStyled,
|
||||||
|
commonMessages,
|
||||||
|
OverflowMenu,
|
||||||
|
SmartClickable,
|
||||||
|
useRelativeTime,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
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 dayjs from 'dayjs'
|
||||||
|
import { Tooltip } from 'floating-vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { copyToClipboard } from '@/helpers/utils'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { Tooltip } from 'floating-vue'
|
|
||||||
|
import { copyToClipboard } from '@/helpers/utils'
|
||||||
|
import type {
|
||||||
|
ProtocolVersion,
|
||||||
|
ServerStatus,
|
||||||
|
ServerWorld,
|
||||||
|
SingleplayerWorld,
|
||||||
|
World,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
|
import { getWorldIdentifier, set_world_display_status } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
@@ -54,8 +61,9 @@ const props = withDefaults(
|
|||||||
playingInstance?: boolean
|
playingInstance?: boolean
|
||||||
playingWorld?: boolean
|
playingWorld?: boolean
|
||||||
startingInstance?: boolean
|
startingInstance?: boolean
|
||||||
supportsQuickPlay?: boolean
|
supportsServerQuickPlay?: boolean
|
||||||
currentProtocol?: number | null
|
supportsWorldQuickPlay?: boolean
|
||||||
|
currentProtocol?: ProtocolVersion | null
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
|
|
||||||
// Server only
|
// Server only
|
||||||
@@ -78,7 +86,8 @@ const props = withDefaults(
|
|||||||
playingInstance: false,
|
playingInstance: false,
|
||||||
playingWorld: false,
|
playingWorld: false,
|
||||||
startingInstance: false,
|
startingInstance: false,
|
||||||
supportsQuickPlay: false,
|
supportsServerQuickPlay: true,
|
||||||
|
supportsWorldQuickPlay: false,
|
||||||
currentProtocol: null,
|
currentProtocol: null,
|
||||||
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
@@ -102,7 +111,8 @@ const serverIncompatible = computed(
|
|||||||
!!props.serverStatus &&
|
!!props.serverStatus &&
|
||||||
!!props.serverStatus.version?.protocol &&
|
!!props.serverStatus.version?.protocol &&
|
||||||
!!props.currentProtocol &&
|
!!props.currentProtocol &&
|
||||||
props.serverStatus.version.protocol !== props.currentProtocol,
|
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
||||||
|
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
||||||
)
|
)
|
||||||
|
|
||||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
@@ -120,14 +130,26 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.a_minecraft_server',
|
id: 'instance.worlds.a_minecraft_server',
|
||||||
defaultMessage: 'A Minecraft Server',
|
defaultMessage: 'A Minecraft Server',
|
||||||
},
|
},
|
||||||
noQuickPlay: {
|
noServerQuickPlay: {
|
||||||
id: 'instance.worlds.no_quick_play',
|
id: 'instance.worlds.no_server_quick_play',
|
||||||
defaultMessage: 'You can only jump straight into worlds on Minecraft 1.20+',
|
defaultMessage: 'You can only jump straight into servers on Minecraft Alpha 1.0.5+',
|
||||||
|
},
|
||||||
|
noSingleplayerQuickPlay: {
|
||||||
|
id: 'instance.worlds.no_singleplayer_quick_play',
|
||||||
|
defaultMessage: 'You can only jump straight into singleplayer worlds on Minecraft 1.20+',
|
||||||
},
|
},
|
||||||
gameAlreadyOpen: {
|
gameAlreadyOpen: {
|
||||||
id: 'instance.worlds.game_already_open',
|
id: 'instance.worlds.game_already_open',
|
||||||
defaultMessage: 'Instance is already open',
|
defaultMessage: 'Instance is already open',
|
||||||
},
|
},
|
||||||
|
noContact: {
|
||||||
|
id: 'instance.worlds.no_contact',
|
||||||
|
defaultMessage: "Server couldn't be contacted",
|
||||||
|
},
|
||||||
|
incompatibleServer: {
|
||||||
|
id: 'instance.worlds.incompatible_server',
|
||||||
|
defaultMessage: 'Server is incompatible',
|
||||||
|
},
|
||||||
copyAddress: {
|
copyAddress: {
|
||||||
id: 'instance.worlds.copy_address',
|
id: 'instance.worlds.copy_address',
|
||||||
defaultMessage: 'Copy address',
|
defaultMessage: 'Copy address',
|
||||||
@@ -136,10 +158,6 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.view_instance',
|
id: 'instance.worlds.view_instance',
|
||||||
defaultMessage: 'View instance',
|
defaultMessage: 'View instance',
|
||||||
},
|
},
|
||||||
playAnyway: {
|
|
||||||
id: 'instance.worlds.play_anyway',
|
|
||||||
defaultMessage: 'Play anyway',
|
|
||||||
},
|
|
||||||
playInstance: {
|
playInstance: {
|
||||||
id: 'instance.worlds.play_instance',
|
id: 'instance.worlds.play_instance',
|
||||||
defaultMessage: 'Play instance',
|
defaultMessage: 'Play instance',
|
||||||
@@ -218,7 +236,8 @@ const messages = defineMessages({
|
|||||||
/>
|
/>
|
||||||
<Tooltip :disabled="!hasPlayersTooltip">
|
<Tooltip :disabled="!hasPlayersTooltip">
|
||||||
<span :class="{ 'cursor-help': hasPlayersTooltip }">
|
<span :class="{ 'cursor-help': hasPlayersTooltip }">
|
||||||
{{ formatNumber(serverStatus.players?.online, false) }} online
|
{{ formatNumber(serverStatus.players?.online, false) }}
|
||||||
|
online
|
||||||
</span>
|
</span>
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
@@ -231,7 +250,8 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" /> Offline
|
<NoSignalIcon aria-hidden="true" stroke-width="3px" class="shrink-0" />
|
||||||
|
Offline
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +261,9 @@ const messages = defineMessages({
|
|||||||
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
world.last_played ? dayjs(world.last_played).format('MMMM D, YYYY [at] h:mm A') : null
|
||||||
"
|
"
|
||||||
class="w-fit shrink-0"
|
class="w-fit shrink-0"
|
||||||
:class="{ 'cursor-help smart-clickable:allow-pointer-events': world.last_played }"
|
:class="{
|
||||||
|
'cursor-help smart-clickable:allow-pointer-events': world.last_played,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="world.last_played">
|
<template v-if="world.last_played">
|
||||||
{{
|
{{
|
||||||
@@ -302,7 +324,6 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||||
<template v-if="world.type === 'singleplayer' || serverStatus">
|
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -315,15 +336,24 @@ const messages = defineMessages({
|
|||||||
<ButtonStyled v-else>
|
<ButtonStyled v-else>
|
||||||
<button
|
<button
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
serverIncompatible
|
world.type == 'server' && !supportsServerQuickPlay
|
||||||
? 'Server is incompatible'
|
? formatMessage(messages.noServerQuickPlay)
|
||||||
: !supportsQuickPlay
|
: world.type == 'singleplayer' && !supportsWorldQuickPlay
|
||||||
? formatMessage(messages.noQuickPlay)
|
? formatMessage(messages.noSingleplayerQuickPlay)
|
||||||
: playingOtherWorld || locked
|
: playingOtherWorld || locked
|
||||||
? formatMessage(messages.gameAlreadyOpen)
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
|
: !serverStatus
|
||||||
|
? formatMessage(messages.noContact)
|
||||||
|
: serverIncompatible
|
||||||
|
? formatMessage(messages.incompatibleServer)
|
||||||
: null
|
: null
|
||||||
"
|
"
|
||||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
:disabled="
|
||||||
|
playingOtherWorld ||
|
||||||
|
startingInstance ||
|
||||||
|
(world.type == 'server' && !supportsServerQuickPlay) ||
|
||||||
|
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
||||||
|
"
|
||||||
@click="emit('play')"
|
@click="emit('play')"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||||
@@ -331,13 +361,6 @@ const messages = defineMessages({
|
|||||||
{{ formatMessage(commonMessages.playButton) }}
|
{{ formatMessage(commonMessages.playButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
|
||||||
<ButtonStyled v-else>
|
|
||||||
<button class="invisible">
|
|
||||||
<PlayIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled circular type="transparent">
|
<ButtonStyled circular type="transparent">
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
@@ -347,11 +370,6 @@ const messages = defineMessages({
|
|||||||
disabled: playingInstance,
|
disabled: playingInstance,
|
||||||
action: () => emit('play-instance'),
|
action: () => emit('play-instance'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'play-anyway',
|
|
||||||
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
|
||||||
action: () => emit('play'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'open-instance',
|
id: 'open-instance',
|
||||||
shown: !!instancePath,
|
shown: !!instancePath,
|
||||||
@@ -417,26 +435,25 @@ const messages = defineMessages({
|
|||||||
<PlayIcon aria-hidden="true" />
|
<PlayIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.playInstance) }}
|
{{ formatMessage(messages.playInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #play-anyway>
|
|
||||||
<PlayIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(messages.playAnyway) }}
|
|
||||||
</template>
|
|
||||||
<template #open-instance>
|
<template #open-instance>
|
||||||
<EyeIcon aria-hidden="true" />
|
<EyeIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.viewInstance) }}
|
{{ formatMessage(messages.viewInstance) }}
|
||||||
</template>
|
</template>
|
||||||
<template #edit>
|
<template #edit>
|
||||||
<EditIcon aria-hidden="true" /> {{ formatMessage(commonMessages.editButton) }}
|
<EditIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(commonMessages.editButton) }}
|
||||||
</template>
|
</template>
|
||||||
<template #open-folder>
|
<template #open-folder>
|
||||||
<FolderOpenIcon aria-hidden="true" />
|
<FolderOpenIcon aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.openFolderButton) }}
|
{{ formatMessage(commonMessages.openFolderButton) }}
|
||||||
</template>
|
</template>
|
||||||
<template #copy-address>
|
<template #copy-address>
|
||||||
<ClipboardCopyIcon aria-hidden="true" /> {{ formatMessage(messages.copyAddress) }}
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.copyAddress) }}
|
||||||
</template>
|
</template>
|
||||||
<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>
|
<template #dont-show-on-home>
|
||||||
<XIcon aria-hidden="true" />
|
<XIcon aria-hidden="true" />
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
import { ref } from 'vue'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
|
||||||
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { handleError } from '@/store/notifications'
|
import { ref } from 'vue'
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
|
|
||||||
|
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
<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, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, 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 HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import {
|
import {
|
||||||
type ServerPackStatus,
|
type DisplayStatus,
|
||||||
edit_server_in_profile,
|
edit_server_in_profile,
|
||||||
|
type ServerPackStatus,
|
||||||
type ServerWorld,
|
type ServerWorld,
|
||||||
set_world_display_status,
|
set_world_display_status,
|
||||||
type DisplayStatus,
|
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, 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 HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||||
import { set_world_display_status, rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
import { rename_world, reset_world_icon, set_world_display_status } from '@/helpers/worlds.ts'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Checkbox } from '@modrinth/ui'
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Checkbox } from '@modrinth/ui'
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const value = defineModel<boolean>({ required: true })
|
const value = defineModel<boolean>({ required: true })
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TeleportDropdownMenu } from '@modrinth/ui'
|
import { TeleportDropdownMenu } from '@modrinth/ui'
|
||||||
|
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||||
|
|
||||||
import type { ServerPackStatus } from '@/helpers/worlds.ts'
|
import type { ServerPackStatus } from '@/helpers/worlds.ts'
|
||||||
import { type MessageDescriptor, defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import cssContent from '@/assets/stylesheets/macFix.css?inline'
|
import cssContent from '@/assets/stylesheets/macFix.css?inline'
|
||||||
|
|
||||||
export async function useCheckDisableMouseover() {
|
export async function useCheckDisableMouseover() {
|
||||||
|
|||||||
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { get_max_memory } from '@/helpers/jre.js'
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
const maxMemory = ref(Math.floor((await get_max_memory()) / 1024))
|
||||||
|
|
||||||
|
const snapPoints = computed(() => {
|
||||||
|
let points = []
|
||||||
|
let memory = 2048
|
||||||
|
|
||||||
|
while (memory <= maxMemory.value) {
|
||||||
|
points.push(memory)
|
||||||
|
memory *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
})
|
||||||
|
|
||||||
|
return { maxMemory, snapPoints }
|
||||||
|
}
|
||||||
@@ -17,6 +17,24 @@ export async function offline_login(name) {
|
|||||||
return await invoke('plugin:auth|offline_login', { name: name })
|
return await invoke('plugin:auth|offline_login', { name: name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
export async function elyby_login(uuid, login, accessToken) {
|
||||||
|
return await invoke('plugin:auth|elyby_login', {
|
||||||
|
uuid,
|
||||||
|
login,
|
||||||
|
accessToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
export async function elyby_auth_authenticate(login, password, clientToken) {
|
||||||
|
return await invoke('plugin:auth|elyby_auth_authenticate', {
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
clientToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate a user with Hydra - part 1.
|
* Authenticate a user with Hydra - part 1.
|
||||||
* This begins the authentication flow quasi-synchronously.
|
* This begins the authentication flow quasi-synchronously.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { fetch } from '@tauri-apps/plugin-http'
|
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
|
||||||
export const useFetch = async (url, item, isSilent) => {
|
export const useFetch = async (url, item, isSilent) => {
|
||||||
try {
|
try {
|
||||||
@@ -11,8 +10,9 @@ export const useFetch = async (url, item, isSilent) => {
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isSilent) {
|
if (!isSilent) {
|
||||||
handleError({ message: `Error fetching ${item}` })
|
throw err
|
||||||
}
|
} else {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import { create } from './profile'
|
import { create } from './profile'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -61,3 +62,31 @@ export async function is_valid_importable_instance(instanceFolder, launcherType)
|
|||||||
export async function get_default_launcher_path(launcherType) {
|
export async function get_default_launcher_path(launcherType) {
|
||||||
return await invoke('plugin:import|get_default_launcher_path', { launcherType })
|
return await invoke('plugin:import|get_default_launcher_path', { launcherType })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch CurseForge profile metadata from profile code
|
||||||
|
/// eg: fetch_curseforge_profile_metadata("eSrNlKNo")
|
||||||
|
export async function fetch_curseforge_profile_metadata(profileCode) {
|
||||||
|
return await invoke('plugin:import|fetch_curseforge_profile_metadata', { profileCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a CurseForge profile from profile code
|
||||||
|
/// eg: import_curseforge_profile("eSrNlKNo")
|
||||||
|
export async function import_curseforge_profile(profileCode) {
|
||||||
|
try {
|
||||||
|
// First, fetch the profile metadata to get the actual name
|
||||||
|
const metadata = await fetch_curseforge_profile_metadata(profileCode)
|
||||||
|
|
||||||
|
// create a basic, empty instance using the actual profile name
|
||||||
|
const profilePath = await create(metadata.name, '1.19.4', 'vanilla', 'latest', null, true)
|
||||||
|
|
||||||
|
const result = await invoke('plugin:import|import_curseforge_profile', {
|
||||||
|
profilePath,
|
||||||
|
profileCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return the profile path for navigation
|
||||||
|
return { result, profilePath }
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export async function get_jre(path) {
|
|||||||
|
|
||||||
// Tests JRE version by running 'java -version' on it.
|
// Tests JRE version by running 'java -version' on it.
|
||||||
// Returns true if the version is valid, and matches given (after extraction)
|
// Returns true if the version is valid, and matches given (after extraction)
|
||||||
export async function test_jre(path, majorVersion, minorVersion) {
|
export async function test_jre(path, majorVersion) {
|
||||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
|
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically installs specified java version
|
// Automatically installs specified java version
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ export async function get_logs_by_filename(profilePath, logType, filename) {
|
|||||||
|
|
||||||
/// Get a profile's log text only by filename
|
/// Get a profile's log text only by filename
|
||||||
export async function get_output_by_filename(profilePath, logType, filename) {
|
export async function get_output_by_filename(profilePath, logType, filename) {
|
||||||
return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, logType, filename })
|
return await invoke('plugin:logs|logs_get_output_by_filename', {
|
||||||
|
profilePath,
|
||||||
|
logType,
|
||||||
|
filename,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete a profile's log by filename
|
/// Delete a profile's log by filename
|
||||||
|
|||||||
@@ -16,3 +16,7 @@ export async function logout() {
|
|||||||
export async function get() {
|
export async function get() {
|
||||||
return await invoke('plugin:mr-auth|get')
|
return await invoke('plugin:mr-auth|get')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cancelLogin() {
|
||||||
|
return await invoke('plugin:mr-auth|cancel_modrinth_login')
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
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
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
import { install_to_existing_profile } from '@/helpers/pack.js'
|
import { install_to_existing_profile } from '@/helpers/pack.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
|
||||||
|
|
||||||
/// Add instance
|
/// Add instance
|
||||||
/*
|
/*
|
||||||
@@ -128,7 +128,10 @@ export async function remove_project(path, projectPath) {
|
|||||||
|
|
||||||
// Update a managed Modrinth profile to a specific version
|
// Update a managed Modrinth profile to a specific version
|
||||||
export async function update_managed_modrinth_version(path, versionId) {
|
export async function update_managed_modrinth_version(path, versionId) {
|
||||||
return await invoke('plugin:profile|profile_update_managed_modrinth_version', { path, versionId })
|
return await invoke('plugin:profile|profile_update_managed_modrinth_version', {
|
||||||
|
path,
|
||||||
|
versionId,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repair a managed Modrinth profile
|
// Repair a managed Modrinth profile
|
||||||
@@ -197,8 +200,8 @@ export async function finish_install(instance) {
|
|||||||
linkedData.version_id,
|
linkedData.version_id,
|
||||||
instance.name,
|
instance.name,
|
||||||
instance.path,
|
instance.path,
|
||||||
).catch(handleError)
|
)
|
||||||
} else {
|
} else {
|
||||||
await install(instance.path, false).catch(handleError)
|
await install(instance.path, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,48 @@
|
|||||||
|
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
applyCapeTexture,
|
||||||
|
createTransparentTexture,
|
||||||
|
disposeCaches,
|
||||||
|
loadTexture,
|
||||||
|
setupSkinModel,
|
||||||
|
} from '@modrinth/utils'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import type { Skin, Cape } from '../skins'
|
|
||||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
|
|
||||||
|
import type { Cape, Skin } from '../skins'
|
||||||
|
import { determineModelType, get_normalized_skin_texture } from '../skins'
|
||||||
|
import { headStorage } from '../storage/head-storage'
|
||||||
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||||
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
|
||||||
|
|
||||||
export interface RenderResult {
|
export interface RenderResult {
|
||||||
forwards: string
|
forwards: string
|
||||||
backwards: string
|
backwards: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawRenderResult {
|
||||||
|
forwards: Blob
|
||||||
|
backwards: Blob
|
||||||
|
}
|
||||||
|
|
||||||
class BatchSkinRenderer {
|
class BatchSkinRenderer {
|
||||||
private renderer: THREE.WebGLRenderer
|
private renderer: THREE.WebGLRenderer | null = null
|
||||||
private readonly scene: THREE.Scene
|
private scene: THREE.Scene | null = null
|
||||||
private readonly camera: THREE.PerspectiveCamera
|
private camera: THREE.PerspectiveCamera | null = null
|
||||||
private currentModel: THREE.Group | null = null
|
private currentModel: THREE.Group | null = null
|
||||||
|
private readonly width: number
|
||||||
|
private readonly height: number
|
||||||
|
|
||||||
constructor(width: number = 360, height: number = 504) {
|
constructor(width: number = 360, height: number = 504) {
|
||||||
|
this.width = width
|
||||||
|
this.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeRenderer(): void {
|
||||||
|
if (this.renderer) return
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = width
|
canvas.width = this.width
|
||||||
canvas.height = height
|
canvas.height = this.height
|
||||||
|
|
||||||
this.renderer = new THREE.WebGLRenderer({
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
@@ -33,10 +55,10 @@ class BatchSkinRenderer {
|
|||||||
this.renderer.toneMapping = THREE.NoToneMapping
|
this.renderer.toneMapping = THREE.NoToneMapping
|
||||||
this.renderer.toneMappingExposure = 10.0
|
this.renderer.toneMappingExposure = 10.0
|
||||||
this.renderer.setClearColor(0x000000, 0)
|
this.renderer.setClearColor(0x000000, 0)
|
||||||
this.renderer.setSize(width, height)
|
this.renderer.setSize(this.width, this.height)
|
||||||
|
|
||||||
this.scene = new THREE.Scene()
|
this.scene = new THREE.Scene()
|
||||||
this.camera = new THREE.PerspectiveCamera(20, width / height, 0.4, 1000)
|
this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
|
||||||
|
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||||
@@ -50,9 +72,12 @@ class BatchSkinRenderer {
|
|||||||
textureUrl: string,
|
textureUrl: string,
|
||||||
modelUrl: string,
|
modelUrl: string,
|
||||||
capeUrl?: string,
|
capeUrl?: string,
|
||||||
capeModelUrl?: string,
|
): Promise<RawRenderResult> {
|
||||||
): Promise<RenderResult> {
|
this.initializeRenderer()
|
||||||
await this.setupModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
|
||||||
|
this.clearScene()
|
||||||
|
|
||||||
|
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
||||||
|
|
||||||
const headPart = this.currentModel!.getObjectByName('Head')
|
const headPart = this.currentModel!.getObjectByName('Head')
|
||||||
let lookAtTarget: [number, number, number]
|
let lookAtTarget: [number, number, number]
|
||||||
@@ -77,35 +102,35 @@ class BatchSkinRenderer {
|
|||||||
private async renderView(
|
private async renderView(
|
||||||
cameraPosition: [number, number, number],
|
cameraPosition: [number, number, number],
|
||||||
lookAtPosition: [number, number, number],
|
lookAtPosition: [number, number, number],
|
||||||
): Promise<string> {
|
): Promise<Blob> {
|
||||||
|
if (!this.camera || !this.renderer || !this.scene) {
|
||||||
|
throw new Error('Renderer not initialized')
|
||||||
|
}
|
||||||
|
|
||||||
this.camera.position.set(...cameraPosition)
|
this.camera.position.set(...cameraPosition)
|
||||||
this.camera.lookAt(...lookAtPosition)
|
this.camera.lookAt(...lookAtPosition)
|
||||||
|
|
||||||
this.renderer.render(this.scene, this.camera)
|
this.renderer.render(this.scene, this.camera)
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
||||||
this.renderer.domElement.toBlob((blob) => {
|
const response = await fetch(dataUrl)
|
||||||
if (blob) {
|
return await response.blob()
|
||||||
const url = URL.createObjectURL(blob)
|
}
|
||||||
resolve(url)
|
|
||||||
|
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||||
|
if (!this.scene) {
|
||||||
|
throw new Error('Renderer not initialized')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { model } = await setupSkinModel(modelUrl, textureUrl)
|
||||||
|
|
||||||
|
if (capeUrl) {
|
||||||
|
const capeTexture = await loadTexture(capeUrl)
|
||||||
|
applyCapeTexture(model, capeTexture)
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Failed to create blob from canvas'))
|
const transparentTexture = createTransparentTexture()
|
||||||
|
applyCapeTexture(model, null, transparentTexture)
|
||||||
}
|
}
|
||||||
}, 'image/png')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setupModel(
|
|
||||||
modelUrl: string,
|
|
||||||
textureUrl: string,
|
|
||||||
capeModelUrl?: string,
|
|
||||||
capeUrl?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
if (this.currentModel) {
|
|
||||||
this.scene.remove(this.currentModel)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { model } = await setupSkinModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
|
||||||
|
|
||||||
const group = new THREE.Group()
|
const group = new THREE.Group()
|
||||||
group.add(model)
|
group.add(model)
|
||||||
@@ -116,8 +141,39 @@ class BatchSkinRenderer {
|
|||||||
this.currentModel = group
|
this.currentModel = group
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private clearScene(): void {
|
||||||
|
if (!this.scene) return
|
||||||
|
|
||||||
|
while (this.scene.children.length > 0) {
|
||||||
|
const child = this.scene.children[0]
|
||||||
|
this.scene.remove(child)
|
||||||
|
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
if (child.geometry) child.geometry.dispose()
|
||||||
|
if (child.material) {
|
||||||
|
if (Array.isArray(child.material)) {
|
||||||
|
child.material.forEach((material) => material.dispose())
|
||||||
|
} else {
|
||||||
|
child.material.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||||
|
directionalLight.castShadow = true
|
||||||
|
directionalLight.position.set(2, 4, 3)
|
||||||
|
this.scene.add(ambientLight)
|
||||||
|
this.scene.add(directionalLight)
|
||||||
|
|
||||||
|
this.currentModel = null
|
||||||
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
|
if (this.renderer) {
|
||||||
this.renderer.dispose()
|
this.renderer.dispose()
|
||||||
|
}
|
||||||
disposeCaches()
|
disposeCaches()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,10 +189,25 @@ function getModelUrlForVariant(variant: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const map = reactive(new Map<string, RenderResult>())
|
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||||
export const headMap = reactive(new Map<string, string>())
|
export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||||
const DEBUG_MODE = false
|
const DEBUG_MODE = false
|
||||||
|
|
||||||
|
let sharedRenderer: BatchSkinRenderer | null = null
|
||||||
|
function getSharedRenderer(): BatchSkinRenderer {
|
||||||
|
if (!sharedRenderer) {
|
||||||
|
sharedRenderer = new BatchSkinRenderer()
|
||||||
|
}
|
||||||
|
return sharedRenderer
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disposeSharedRenderer(): void {
|
||||||
|
if (sharedRenderer) {
|
||||||
|
sharedRenderer.dispose()
|
||||||
|
sharedRenderer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||||
const validKeys = new Set<string>()
|
const validKeys = new Set<string>()
|
||||||
const validHeadKeys = new Set<string>()
|
const validHeadKeys = new Set<string>()
|
||||||
@@ -150,7 +221,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||||
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys)
|
await headStorage.cleanupInvalidKeys(validHeadKeys)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to cleanup unused skin previews:', error)
|
console.warn('Failed to cleanup unused skin previews:', error)
|
||||||
}
|
}
|
||||||
@@ -229,13 +300,17 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
|||||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputCanvas.toBlob((blob) => {
|
outputCanvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
resolve(blob)
|
resolve(blob)
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Failed to create blob from canvas'))
|
reject(new Error('Failed to create blob from canvas'))
|
||||||
}
|
}
|
||||||
}, 'image/png')
|
},
|
||||||
|
'image/webp',
|
||||||
|
0.9,
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
}
|
}
|
||||||
@@ -252,35 +327,24 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
|||||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||||
const headKey = `${skin.texture_key}-head`
|
const headKey = `${skin.texture_key}-head`
|
||||||
|
|
||||||
if (headMap.has(headKey)) {
|
if (headBlobUrlMap.has(headKey)) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
const url = headMap.get(headKey)!
|
const url = headBlobUrlMap.get(headKey)!
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
headMap.delete(headKey)
|
headBlobUrlMap.delete(headKey)
|
||||||
} else {
|
} else {
|
||||||
return headMap.get(headKey)!
|
return headBlobUrlMap.get(headKey)!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const cached = await skinPreviewStorage.retrieve(headKey)
|
|
||||||
if (cached && typeof cached === 'string') {
|
|
||||||
headMap.set(headKey, cached)
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to retrieve cached head render:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const skinUrl = await get_normalized_skin_texture(skin)
|
const skinUrl = await get_normalized_skin_texture(skin)
|
||||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||||
const headUrl = URL.createObjectURL(headBlob)
|
const headUrl = URL.createObjectURL(headBlob)
|
||||||
|
|
||||||
headMap.set(headKey, headUrl)
|
headBlobUrlMap.set(headKey, headUrl)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - skinPreviewStorage.store expects a RenderResult, but we are storing a string url.
|
await headStorage.store(headKey, headBlob)
|
||||||
await skinPreviewStorage.store(headKey, headUrl)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to store head render in persistent storage:', error)
|
console.warn('Failed to store head render in persistent storage:', error)
|
||||||
}
|
}
|
||||||
@@ -293,30 +357,49 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||||
const renderer = new BatchSkinRenderer()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const skinKeys = skins.map(
|
||||||
|
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
||||||
|
)
|
||||||
|
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
|
||||||
|
|
||||||
|
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
|
||||||
|
skinPreviewStorage.batchRetrieve(skinKeys),
|
||||||
|
headStorage.batchRetrieve(headKeys),
|
||||||
|
])
|
||||||
|
|
||||||
|
for (let i = 0; i < skins.length; i++) {
|
||||||
|
const skinKey = skinKeys[i]
|
||||||
|
const headKey = headKeys[i]
|
||||||
|
|
||||||
|
const rawCached = cachedSkinPreviews[skinKey]
|
||||||
|
if (rawCached) {
|
||||||
|
const cached: RenderResult = {
|
||||||
|
forwards: URL.createObjectURL(rawCached.forwards),
|
||||||
|
backwards: URL.createObjectURL(rawCached.backwards),
|
||||||
|
}
|
||||||
|
skinBlobUrlMap.set(skinKey, cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedHead = cachedHeadPreviews[headKey]
|
||||||
|
if (cachedHead) {
|
||||||
|
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const skin of skins) {
|
for (const skin of skins) {
|
||||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||||
|
|
||||||
if (map.has(key)) {
|
if (skinBlobUrlMap.has(key)) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
const result = map.get(key)!
|
const result = skinBlobUrlMap.get(key)!
|
||||||
URL.revokeObjectURL(result.forwards)
|
URL.revokeObjectURL(result.forwards)
|
||||||
URL.revokeObjectURL(result.backwards)
|
URL.revokeObjectURL(result.backwards)
|
||||||
map.delete(key)
|
skinBlobUrlMap.delete(key)
|
||||||
} else continue
|
} else continue
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const renderer = getSharedRenderer()
|
||||||
const cached = await skinPreviewStorage.retrieve(key)
|
|
||||||
if (cached) {
|
|
||||||
map.set(key, cached)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to retrieve cached skin preview:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
let variant = skin.variant
|
let variant = skin.variant
|
||||||
if (variant === 'UNKNOWN') {
|
if (variant === 'UNKNOWN') {
|
||||||
@@ -330,25 +413,35 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
|||||||
|
|
||||||
const modelUrl = getModelUrlForVariant(variant)
|
const modelUrl = getModelUrlForVariant(variant)
|
||||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
||||||
const renderResult = await renderer.renderSkin(
|
const rawRenderResult = await renderer.renderSkin(
|
||||||
await get_normalized_skin_texture(skin),
|
await get_normalized_skin_texture(skin),
|
||||||
modelUrl,
|
modelUrl,
|
||||||
cape?.texture,
|
cape?.texture,
|
||||||
CapeModel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
map.set(key, renderResult)
|
const renderResult: RenderResult = {
|
||||||
|
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
||||||
|
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
||||||
|
}
|
||||||
|
|
||||||
|
skinBlobUrlMap.set(key, renderResult)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await skinPreviewStorage.store(key, renderResult)
|
await skinPreviewStorage.store(key, rawRenderResult)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to store skin preview in persistent storage:', error)
|
console.warn('Failed to store skin preview in persistent storage:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headKey = `${skin.texture_key}-head`
|
||||||
|
if (!headBlobUrlMap.has(headKey)) {
|
||||||
await generateHeadRender(skin)
|
await generateHeadRender(skin)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
renderer.dispose()
|
disposeSharedRenderer()
|
||||||
await cleanupUnusedPreviews(skins)
|
await cleanupUnusedPreviews(skins)
|
||||||
|
|
||||||
|
await skinPreviewStorage.debugCalculateStorage()
|
||||||
|
await headStorage.debugCalculateStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
|
||||||
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
import type { Hooks, MemorySettings, WindowSize } from '@/helpers/types'
|
||||||
|
import type { ColorTheme, FeatureFlag } from '@/store/theme.ts'
|
||||||
|
|
||||||
// Settings object
|
// Settings object
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export interface Cape {
|
export interface Cape {
|
||||||
id: string
|
id: string
|
||||||
@@ -39,7 +38,7 @@ export const DEFAULT_MODELS: Record<string, SkinModel> = {
|
|||||||
|
|
||||||
export function filterSavedSkins(list: Skin[]) {
|
export function filterSavedSkins(list: Skin[]) {
|
||||||
const customSkins = list.filter((s) => s.source !== 'default')
|
const customSkins = list.filter((s) => s.source !== 'default')
|
||||||
fixUnknownSkins(customSkins).catch(handleError)
|
fixUnknownSkins(customSkins)
|
||||||
return customSkins
|
return customSkins
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,15 +61,12 @@ export async function determineModelType(texture: string): Promise<'SLIM' | 'CLA
|
|||||||
|
|
||||||
context.drawImage(image, 0, 0)
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
const armX = 44
|
const armX = 54
|
||||||
const armY = 16
|
const armY = 20
|
||||||
const armWidth = 4
|
const armWidth = 2
|
||||||
const armHeight = 12
|
const armHeight = 12
|
||||||
|
|
||||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||||
|
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
|
||||||
for (let y = 0; y < armHeight; y++) {
|
|
||||||
const alphaIndex = (3 + y * armWidth) * 4 + 3
|
|
||||||
if (imageData[alphaIndex] !== 0) {
|
if (imageData[alphaIndex] !== 0) {
|
||||||
resolve('CLASSIC')
|
resolve('CLASSIC')
|
||||||
return
|
return
|
||||||
|
|||||||
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
interface StoredHead {
|
||||||
|
blob: Blob
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HeadStorage {
|
||||||
|
private dbName = 'head-storage'
|
||||||
|
private version = 1
|
||||||
|
private db: IDBDatabase | null = null
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(this.dbName, this.version)
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => {
|
||||||
|
this.db = request.result
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onupgradeneeded = () => {
|
||||||
|
const db = request.result
|
||||||
|
if (!db.objectStoreNames.contains('heads')) {
|
||||||
|
db.createObjectStore('heads')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async store(key: string, blob: Blob): Promise<void> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||||
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
|
const storedHead: StoredHead = {
|
||||||
|
blob,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = store.put(storedHead, key)
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieve(key: string): Promise<string | null> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||||
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = store.get(key)
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const result = request.result as StoredHead | undefined
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
resolve(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(result.blob)
|
||||||
|
resolve(url)
|
||||||
|
}
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||||
|
const store = transaction.objectStore('heads')
|
||||||
|
const results: Record<string, Blob | null> = {}
|
||||||
|
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
let completedRequests = 0
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
resolve(results)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const request = store.get(key)
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const result = request.result as StoredHead | undefined
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
results[key] = result.blob
|
||||||
|
} else {
|
||||||
|
results[key] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
completedRequests++
|
||||||
|
if (completedRequests === keys.length) {
|
||||||
|
resolve(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
results[key] = null
|
||||||
|
completedRequests++
|
||||||
|
if (completedRequests === keys.length) {
|
||||||
|
resolve(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||||
|
const store = transaction.objectStore('heads')
|
||||||
|
let deletedCount = 0
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = store.openCursor()
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
const key = cursor.primaryKey as string
|
||||||
|
|
||||||
|
if (!validKeys.has(key)) {
|
||||||
|
const deleteRequest = cursor.delete()
|
||||||
|
deleteRequest.onsuccess = () => {
|
||||||
|
deletedCount++
|
||||||
|
}
|
||||||
|
deleteRequest.onerror = () => {
|
||||||
|
console.warn('Failed to delete invalid head entry:', key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.continue()
|
||||||
|
} else {
|
||||||
|
resolve(deletedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async debugCalculateStorage(): Promise<void> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||||
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
|
let totalSize = 0
|
||||||
|
let count = 0
|
||||||
|
const entries: Array<{ key: string; size: number }> = []
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = store.openCursor()
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
const key = cursor.primaryKey as string
|
||||||
|
const value = cursor.value as StoredHead
|
||||||
|
|
||||||
|
const entrySize = value.blob.size
|
||||||
|
totalSize += entrySize
|
||||||
|
count++
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
key,
|
||||||
|
size: entrySize,
|
||||||
|
})
|
||||||
|
|
||||||
|
cursor.continue()
|
||||||
|
} else {
|
||||||
|
console.group('🗄️ Head Storage Debug Info')
|
||||||
|
console.log(`Total entries: ${count}`)
|
||||||
|
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||||
|
console.log(
|
||||||
|
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (entries.length > 0) {
|
||||||
|
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||||
|
console.log(
|
||||||
|
'Largest entry:',
|
||||||
|
sortedEntries[0].key,
|
||||||
|
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
'Smallest entry:',
|
||||||
|
sortedEntries[sortedEntries.length - 1].key,
|
||||||
|
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAll(): Promise<void> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||||
|
const store = transaction.objectStore('heads')
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = store.clear()
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const headStorage = new HeadStorage()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { RenderResult } from '../rendering/batch-skin-renderer'
|
import type { RawRenderResult } from '../rendering/batch-skin-renderer'
|
||||||
|
|
||||||
interface StoredPreview {
|
interface StoredPreview {
|
||||||
forwards: Blob
|
forwards: Blob
|
||||||
@@ -30,18 +30,15 @@ export class SkinPreviewStorage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async store(key: string, result: RenderResult): Promise<void> {
|
async store(key: string, result: RawRenderResult): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const forwardsBlob = await fetch(result.forwards).then((r) => r.blob())
|
|
||||||
const backwardsBlob = await fetch(result.backwards).then((r) => r.blob())
|
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||||
const store = transaction.objectStore('previews')
|
const store = transaction.objectStore('previews')
|
||||||
|
|
||||||
const storedPreview: StoredPreview = {
|
const storedPreview: StoredPreview = {
|
||||||
forwards: forwardsBlob,
|
forwards: result.forwards,
|
||||||
backwards: backwardsBlob,
|
backwards: result.backwards,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +50,7 @@ export class SkinPreviewStorage {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieve(key: string): Promise<RenderResult | null> {
|
async retrieve(key: string): Promise<RawRenderResult | null> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||||
@@ -70,14 +67,56 @@ export class SkinPreviewStorage {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const forwards = URL.createObjectURL(result.forwards)
|
resolve({ forwards: result.forwards, backwards: result.backwards })
|
||||||
const backwards = URL.createObjectURL(result.backwards)
|
|
||||||
resolve({ forwards, backwards })
|
|
||||||
}
|
}
|
||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||||
|
const store = transaction.objectStore('previews')
|
||||||
|
const results: Record<string, RawRenderResult | null> = {}
|
||||||
|
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
let completedRequests = 0
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
resolve(results)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const request = store.get(key)
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
const result = request.result as StoredPreview | undefined
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
results[key] = { forwards: result.forwards, backwards: result.backwards }
|
||||||
|
} else {
|
||||||
|
results[key] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
completedRequests++
|
||||||
|
if (completedRequests === keys.length) {
|
||||||
|
resolve(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
results[key] = null
|
||||||
|
completedRequests++
|
||||||
|
if (completedRequests === keys.length) {
|
||||||
|
resolve(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
@@ -113,6 +152,67 @@ export class SkinPreviewStorage {
|
|||||||
request.onerror = () => reject(request.error)
|
request.onerror = () => reject(request.error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async debugCalculateStorage(): Promise<void> {
|
||||||
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
|
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||||
|
const store = transaction.objectStore('previews')
|
||||||
|
|
||||||
|
let totalSize = 0
|
||||||
|
let count = 0
|
||||||
|
const entries: Array<{ key: string; size: number }> = []
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = store.openCursor()
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
const key = cursor.primaryKey as string
|
||||||
|
const value = cursor.value as StoredPreview
|
||||||
|
|
||||||
|
const entrySize = value.forwards.size + value.backwards.size
|
||||||
|
totalSize += entrySize
|
||||||
|
count++
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
key,
|
||||||
|
size: entrySize,
|
||||||
|
})
|
||||||
|
|
||||||
|
cursor.continue()
|
||||||
|
} else {
|
||||||
|
console.group('🗄️ Skin Preview Storage Debug Info')
|
||||||
|
console.log(`Total entries: ${count}`)
|
||||||
|
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||||
|
console.log(
|
||||||
|
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (entries.length > 0) {
|
||||||
|
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||||
|
console.log(
|
||||||
|
'Largest entry:',
|
||||||
|
sortedEntries[0].key,
|
||||||
|
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
'Smallest entry:',
|
||||||
|
sortedEntries[sortedEntries.length - 1].key,
|
||||||
|
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const skinPreviewStorage = new SkinPreviewStorage()
|
export const skinPreviewStorage = new SkinPreviewStorage()
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { getArtifact, getOS } from '@/helpers/utils.js'
|
import { initUpdateLauncher, getOS } from '@/helpers/utils.js'
|
||||||
|
|
||||||
|
|
||||||
export const allowState = ref(false)
|
export const allowState = ref(false)
|
||||||
export const installState = ref(false)
|
export const installState = ref(false)
|
||||||
export const updateState = ref(false)
|
export const updateState = ref(false)
|
||||||
export const latestBetaCommitTruncatedSha = ref('')
|
|
||||||
export const latestBetaCommitLink = ref('')
|
|
||||||
export const launcherUrl = 'https://www.astralium.su/get/ar'
|
|
||||||
|
|
||||||
const os = ref('')
|
const currentOS = ref('')
|
||||||
const releaseLink = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/releases/latest`
|
const releaseLink = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/releases/latest`
|
||||||
const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`]
|
const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`]
|
||||||
const osNames = ['macos', 'windows', 'linux']
|
|
||||||
const macExtension = `.dmg` // MacOS file type for download
|
const osList = ['macos', 'windows', 'linux']
|
||||||
const windowsExtension = `.msi` // Windows file type for download
|
const macExtensionList = ['.dmg', '.pkg']
|
||||||
const blacklistedBuilds = [
|
const windowsExtensionList = ['.exe', '.msi']
|
||||||
|
|
||||||
|
const blacklistPrefixes = [
|
||||||
`dev`,
|
`dev`,
|
||||||
`nightly`,
|
`nightly`,
|
||||||
`dirty`,
|
`dirty`,
|
||||||
@@ -26,110 +24,73 @@ const blacklistedBuilds = [
|
|||||||
`dirty_nightly`,
|
`dirty_nightly`,
|
||||||
] // This is blacklisted builds for download. For example, file.startsWith('dev') is not allowed.
|
] // This is blacklisted builds for download. For example, file.startsWith('dev') is not allowed.
|
||||||
|
|
||||||
/**
|
export async function getRemote(isDownloadState) {
|
||||||
* Asynchronous function to get remote data and handle updates and downloads.
|
var releaseData = null;
|
||||||
*
|
var result = false;
|
||||||
* @param {boolean} elementIdBool - Indicates whether to disable an element ID.
|
try {
|
||||||
* @param {boolean} downloadArtifactBool - Indicates whether to download an artifact.
|
const response = await fetch(releaseLink);
|
||||||
*/
|
|
||||||
export async function getRemote(elementIdBool, downloadArtifactBool) {
|
|
||||||
fetch(releaseLink)
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(response.status)
|
throw new Error(response.status);
|
||||||
}
|
}
|
||||||
return response.json()
|
const remoteData = await response.json();
|
||||||
})
|
currentOS.value = await getOS();
|
||||||
.then(async (data) => {
|
const remoteLatestReleaseTag = remoteData.tag_name;
|
||||||
os.value = await getOS()
|
releaseData = document.getElementById('releaseData');
|
||||||
const latestRelease = data.name
|
const remoteVersion = releaseData ? (releaseData.textContent = remoteLatestReleaseTag) : remoteLatestReleaseTag;
|
||||||
let remoteVersion = undefined
|
|
||||||
|
|
||||||
if (!elementIdBool) {
|
if (osList.includes(currentOS.value.toLowerCase())) {
|
||||||
const releaseData = document.getElementById('releaseData')
|
const localVersion = await getVersion();
|
||||||
if (releaseData == null) {
|
const isUpdateAvailable = !remoteVersion.includes(localVersion);
|
||||||
console.error('Release data element not found.')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
releaseData.textContent = latestRelease
|
|
||||||
remoteVersion = `${releaseData.textContent}`
|
|
||||||
} else {
|
|
||||||
remoteVersion = latestRelease
|
|
||||||
}
|
|
||||||
if (osNames.includes(os.value.toLowerCase())) {
|
|
||||||
if (remoteVersion.startsWith('v' + await getVersion())) {
|
|
||||||
updateState.value = false
|
|
||||||
allowState.value = false
|
|
||||||
} else {
|
|
||||||
updateState.value = true
|
|
||||||
allowState.value = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateState.value = false
|
|
||||||
allowState.value = false
|
|
||||||
}
|
|
||||||
console.log('Update available state is', updateState.value)
|
|
||||||
console.log('Remote version is', remoteVersion)
|
|
||||||
console.log('Local version is', await getVersion())
|
|
||||||
console.log('Operating System is', os.value)
|
|
||||||
|
|
||||||
if (downloadArtifactBool) {
|
updateState.value = isUpdateAvailable;
|
||||||
installState.value = true
|
allowState.value = isUpdateAvailable;
|
||||||
const builds = data.assets
|
} else {
|
||||||
const fileName = getInstaller(getExtension(), builds)
|
updateState.value = false;
|
||||||
if (fileName != null) {
|
allowState.value = false;
|
||||||
await getArtifact(fileName[1], fileName[0], os.value, true)
|
|
||||||
}
|
}
|
||||||
installState.value = false
|
if (isDownloadState) {
|
||||||
|
installState.value = true;
|
||||||
|
const builds = remoteData.assets;
|
||||||
|
const fileName = getInstaller(getExtension(), builds);
|
||||||
|
result = fileName ? await initUpdateLauncher(fileName[1], fileName[0], currentOS.value, true) : false;
|
||||||
|
installState.value = false;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((error) => {
|
console.log('Update available state is', updateState.value);
|
||||||
console.error(failedFetch[0], error)
|
console.log('Remote version is', remoteVersion);
|
||||||
if (!elementIdBool) {
|
console.log('Local version is', await getVersion());
|
||||||
const errorData = document.getElementById('releaseData')
|
console.log('Operating System is', currentOS.value);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(failedFetch[0], error);
|
||||||
|
if (!releaseData) {
|
||||||
|
const errorData = document.getElementById('releaseData');
|
||||||
if (errorData) {
|
if (errorData) {
|
||||||
errorData.textContent = `${error.message}`
|
errorData.textContent = `${error.message}`;
|
||||||
|
}
|
||||||
|
updateState.value = false;
|
||||||
|
allowState.value = false;
|
||||||
|
installState.value = false;
|
||||||
}
|
}
|
||||||
updateState.value = false
|
|
||||||
allowState.value = false
|
|
||||||
installState.value = false
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the installer for a specific operating system.
|
|
||||||
*
|
|
||||||
* @param {string} osExtension - The file extension of the installer.
|
|
||||||
* @param {Array} builds - The list of builds.
|
|
||||||
* @return {Array|null} An array containing the installer name and URL if found, or null if not found.
|
|
||||||
*/
|
|
||||||
function getInstaller(osExtension, builds) {
|
function getInstaller(osExtension, builds) {
|
||||||
for (let i of builds) {
|
console.log(osExtension, builds)
|
||||||
let blacklistedItem = false
|
for (const build of builds) {
|
||||||
blacklistedBuilds.forEach((item) => {
|
if (blacklistPrefixes.some(prefix => build.name.startsWith(prefix))) {
|
||||||
if (i.name.startsWith(item)) {
|
continue;
|
||||||
return (blacklistedItem = true)
|
|
||||||
}
|
}
|
||||||
})
|
if (osExtension.some(ext => build.name.endsWith(ext))) {
|
||||||
if (i.name.endsWith(osExtension) && !blacklistedItem) {
|
console.log(build.name, build.browser_download_url);
|
||||||
console.log(i.browser_download_url)
|
return [build.name, build.browser_download_url];
|
||||||
return [i.name, i.browser_download_url]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A function to get the extension based on the operating system.
|
|
||||||
*
|
|
||||||
* @return {string} The extension based on the operating system.
|
|
||||||
*/
|
|
||||||
function getExtension() {
|
function getExtension() {
|
||||||
if (os.value.toLowerCase() == osNames[0]) {
|
return osList.find(osName => osName === currentOS.value.toLowerCase())?.endsWith('macos')
|
||||||
return macExtension
|
? macExtensionList
|
||||||
} else if (os.value.toLowerCase() == osNames[1]) {
|
: windowsExtensionList;
|
||||||
return windowsExtension
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
|
import { get_full_path, get_mod_full_path } from '@/helpers/profile'
|
||||||
|
|
||||||
export async function isDev() {
|
export async function isDev() {
|
||||||
return await invoke('is_dev')
|
return await invoke('is_dev')
|
||||||
}
|
}
|
||||||
@@ -10,9 +11,20 @@ export async function getOS() {
|
|||||||
return await invoke('plugin:utils|get_os')
|
return await invoke('plugin:utils|get_os')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getArtifact(downloadurl, filename, ostype, autoupdatesupported) {
|
// [AR] Feature. Updater
|
||||||
console.log('Downloading build', downloadurl, filename, ostype, autoupdatesupported)
|
export async function initUpdateLauncher(downloadUrl, filename, osType, autoUpdateSupported) {
|
||||||
return await invoke('plugin:utils|get_artifact', { downloadurl, filename, ostype, autoupdatesupported })
|
console.log('Downloading build', downloadUrl, filename, osType, autoUpdateSupported)
|
||||||
|
return await invoke('plugin:utils|init_update_launcher', { downloadUrl, filename, osType, autoUpdateSupported })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] Migration. Patch
|
||||||
|
export async function applyMigrationFix(eol) {
|
||||||
|
return await invoke('plugin:utils|apply_migration_fix', { eol })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] Feature. Ely.by
|
||||||
|
export async function initAuthlibPatching(minecraftVersion, isMojang) {
|
||||||
|
return await invoke('plugin:utils|init_authlib_patching', { minecraftVersion, isMojang })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openPath(path) {
|
export async function openPath(path) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { autoToHTML } from '@geometrically/minecraft-motd-parser'
|
||||||
|
import type { GameVersion } from '@modrinth/ui'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { get_full_path } from '@/helpers/profile'
|
import { get_full_path } from '@/helpers/profile'
|
||||||
import { openPath } from '@/helpers/utils'
|
import { openPath } from '@/helpers/utils'
|
||||||
import { autoToHTML } from '@geometrically/minecraft-motd-parser'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type { GameVersion } from '@modrinth/ui'
|
|
||||||
|
|
||||||
type BaseWorld = {
|
type BaseWorld = {
|
||||||
name: string
|
name: string
|
||||||
@@ -51,6 +52,7 @@ export type ServerStatus = {
|
|||||||
version?: {
|
version?: {
|
||||||
name: string
|
name: string
|
||||||
protocol: number
|
protocol: number
|
||||||
|
legacy: boolean
|
||||||
}
|
}
|
||||||
favicon?: string
|
favicon?: string
|
||||||
enforces_secure_chat: boolean
|
enforces_secure_chat: boolean
|
||||||
@@ -70,11 +72,17 @@ export interface Chat {
|
|||||||
|
|
||||||
export type ServerData = {
|
export type ServerData = {
|
||||||
refreshing: boolean
|
refreshing: boolean
|
||||||
|
lastSuccessfulRefresh?: number
|
||||||
status?: ServerStatus
|
status?: ServerStatus
|
||||||
rawMotd?: string | Chat
|
rawMotd?: string | Chat
|
||||||
renderedMotd?: string
|
renderedMotd?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProtocolVersion = {
|
||||||
|
version: number
|
||||||
|
legacy: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export async function get_recent_worlds(
|
export async function get_recent_worlds(
|
||||||
limit: number,
|
limit: number,
|
||||||
displayStatuses?: DisplayStatus[],
|
displayStatuses?: DisplayStatus[],
|
||||||
@@ -156,13 +164,13 @@ export async function remove_server_from_profile(path: string, index: number): P
|
|||||||
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_profile_protocol_version(path: string): Promise<number | null> {
|
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
||||||
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_server_status(
|
export async function get_server_status(
|
||||||
address: string,
|
address: string,
|
||||||
protocolVersion: number | null = null,
|
protocolVersion: ProtocolVersion | null = null,
|
||||||
): Promise<ServerStatus> {
|
): Promise<ServerStatus> {
|
||||||
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
||||||
}
|
}
|
||||||
@@ -206,30 +214,39 @@ export function isServerWorld(world: World): world is ServerWorld {
|
|||||||
|
|
||||||
export async function refreshServerData(
|
export async function refreshServerData(
|
||||||
serverData: ServerData,
|
serverData: ServerData,
|
||||||
protocolVersion: number | null,
|
protocolVersion: ProtocolVersion | null,
|
||||||
address: string,
|
address: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const refreshTime = Date.now()
|
||||||
serverData.refreshing = true
|
serverData.refreshing = true
|
||||||
await get_server_status(address, protocolVersion)
|
await get_server_status(address, protocolVersion)
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
|
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
||||||
|
// Don't update if there was a more recent successful refresh
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serverData.lastSuccessfulRefresh = Date.now()
|
||||||
serverData.status = status
|
serverData.status = status
|
||||||
if (status.description) {
|
if (status.description) {
|
||||||
serverData.rawMotd = status.description
|
serverData.rawMotd = status.description
|
||||||
serverData.renderedMotd = autoToHTML(status.description)
|
serverData.renderedMotd = autoToHTML(status.description)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
|
||||||
console.error(`Refreshing addr: ${address}`, err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
serverData.refreshing = false
|
serverData.refreshing = false
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
||||||
|
if (!protocolVersion?.legacy) {
|
||||||
|
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshServers(
|
export function refreshServers(
|
||||||
worlds: World[],
|
worlds: World[],
|
||||||
serverData: Record<string, ServerData>,
|
serverData: Record<string, ServerData>,
|
||||||
protocolVersion: number | null,
|
protocolVersion: ProtocolVersion | null,
|
||||||
) {
|
) {
|
||||||
const servers = worlds.filter(isServerWorld)
|
const servers = worlds.filter(isServerWorld)
|
||||||
servers.forEach((server) => {
|
servers.forEach((server) => {
|
||||||
@@ -243,10 +260,8 @@ export async function refreshServers(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||||
Promise.all(
|
Object.keys(serverData).forEach((address) =>
|
||||||
Object.keys(serverData).map((address) =>
|
|
||||||
refreshServerData(serverData[address], protocolVersion, address),
|
refreshServerData(serverData[address], protocolVersion, address),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,15 +312,24 @@ export async function refreshWorlds(instancePath: string): Promise<World[]> {
|
|||||||
return worlds ?? []
|
return worlds ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIRST_QUICK_PLAY_VERSION = '23w14a'
|
export function hasServerQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
||||||
|
if (!gameVersions.length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
export function hasQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
||||||
|
const targetIndex = gameVersions.findIndex((v) => v.version === 'a1.0.5_01')
|
||||||
|
|
||||||
|
return versionIndex === -1 || targetIndex === -1 || versionIndex <= targetIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
||||||
if (!gameVersions.length) {
|
if (!gameVersions.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
||||||
const targetIndex = gameVersions.findIndex((v) => v.version === FIRST_QUICK_PLAY_VERSION)
|
const targetIndex = gameVersions.findIndex((v) => v.version === '23w14a')
|
||||||
|
|
||||||
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,11 +377,17 @@
|
|||||||
"instance.worlds.hardcore": {
|
"instance.worlds.hardcore": {
|
||||||
"message": "Hardcore mode"
|
"message": "Hardcore mode"
|
||||||
},
|
},
|
||||||
"instance.worlds.no_quick_play": {
|
"instance.worlds.incompatible_server": {
|
||||||
"message": "You can only jump straight into worlds on Minecraft 1.20+"
|
"message": "Server is incompatible"
|
||||||
},
|
},
|
||||||
"instance.worlds.play_anyway": {
|
"instance.worlds.no_contact": {
|
||||||
"message": "Play anyway"
|
"message": "Server couldn't be contacted"
|
||||||
|
},
|
||||||
|
"instance.worlds.no_server_quick_play": {
|
||||||
|
"message": "You can only jump straight into servers on Minecraft Alpha 1.0.5+"
|
||||||
|
},
|
||||||
|
"instance.worlds.no_singleplayer_quick_play": {
|
||||||
|
"message": "You can only jump straight into singleplayer worlds on Minecraft 1.20+"
|
||||||
},
|
},
|
||||||
"instance.worlds.play_instance": {
|
"instance.worlds.play_instance": {
|
||||||
"message": "Play instance"
|
"message": "Play instance"
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import router from '@/routes'
|
|
||||||
import App from '@/App.vue'
|
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
import FloatingVue from 'floating-vue'
|
|
||||||
import 'floating-vue/dist/style.css'
|
import 'floating-vue/dist/style.css'
|
||||||
import { createPlugin } from '@vintl/vintl/plugin'
|
|
||||||
import * as Sentry from '@sentry/vue'
|
import * as Sentry from '@sentry/vue'
|
||||||
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
||||||
|
import { createPlugin } from '@vintl/vintl/plugin'
|
||||||
|
import FloatingVue from 'floating-vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
import App from '@/App.vue'
|
||||||
|
import router from '@/routes'
|
||||||
|
|
||||||
const VIntlPlugin = createPlugin({
|
const VIntlPlugin = createPlugin({
|
||||||
controllerOpts: {
|
controllerOpts: {
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||||
import type { Ref } from 'vue'
|
|
||||||
import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets'
|
|
||||||
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
SearchFilterControl,
|
|
||||||
SearchSidebarFilter,
|
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DropdownSelect,
|
DropdownSelect,
|
||||||
|
injectNotificationManager,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
SearchFilterControl,
|
||||||
|
SearchSidebarFilter,
|
||||||
useSearch,
|
useSearch,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { handleError } from '@/store/state'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
import type { Ref } from 'vue'
|
||||||
|
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||||
import type { LocationQuery } from 'vue-router'
|
import type { LocationQuery } from 'vue-router'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
|
||||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import { get_search_results } from '@/helpers/cache.js'
|
|
||||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
|
||||||
import type Instance from '@/components/ui/Instance.vue'
|
import type Instance from '@/components/ui/Instance.vue'
|
||||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import SearchCard from '@/components/ui/SearchCard.vue'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { get_search_results } from '@/helpers/cache.js'
|
||||||
|
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
|
||||||
|
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
|
||||||
|
const { handleError } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -160,6 +162,8 @@ const {
|
|||||||
createPageParams,
|
createPageParams,
|
||||||
} = useSearch(projectTypes, tags, instanceFilters)
|
} = useSearch(projectTypes, tags, instanceFilters)
|
||||||
|
|
||||||
|
const previousFilterState = ref('')
|
||||||
|
|
||||||
const offline = ref(!navigator.onLine)
|
const offline = ref(!navigator.onLine)
|
||||||
window.addEventListener('offline', () => {
|
window.addEventListener('offline', () => {
|
||||||
offline.value = true
|
offline.value = true
|
||||||
@@ -220,7 +224,20 @@ async function refreshSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.value = rawResults.result
|
results.value = rawResults.result
|
||||||
currentPage.value = Math.max(1, Math.min(pageCount.value, currentPage.value))
|
|
||||||
|
const currentFilterState = JSON.stringify({
|
||||||
|
query: query.value,
|
||||||
|
filters: currentFilters.value,
|
||||||
|
sort: currentSortType.value,
|
||||||
|
maxResults: maxResults.value,
|
||||||
|
projectTypes: projectTypes.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (previousFilterState.value && previousFilterState.value !== currentFilterState) {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFilterState.value = currentFilterState
|
||||||
|
|
||||||
const persistentParams: LocationQuery = {}
|
const persistentParams: LocationQuery = {}
|
||||||
|
|
||||||
@@ -266,6 +283,7 @@ async function onSearchChangeToTop() {
|
|||||||
|
|
||||||
function clearSearch() {
|
function clearSearch() {
|
||||||
query.value = ''
|
query.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -379,6 +397,15 @@ const handleOptionsClick = (args) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await refreshSearch()
|
await refreshSearch()
|
||||||
|
|
||||||
|
// Initialize previousFilterState after first search
|
||||||
|
previousFilterState.value = JSON.stringify({
|
||||||
|
query: query.value,
|
||||||
|
filters: currentFilters.value,
|
||||||
|
sort: currentSortType.value,
|
||||||
|
maxResults: maxResults.value,
|
||||||
|
projectTypes: projectTypes.value,
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user