Switch to official launcher auth (#1118)
* Switch to official launcher auth * add debug info * Fix build
This commit is contained in:
parent
76447019c0
commit
2877919639
44
.github/workflows/cli-build.yml
vendored
44
.github/workflows/cli-build.yml
vendored
@ -1,44 +0,0 @@
|
|||||||
name: CLI Build + Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./theseus_cli
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Rust setup
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rustfmt, clippy
|
|
||||||
|
|
||||||
- name: Rust cache
|
|
||||||
uses: swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src-tauri -> target'
|
|
||||||
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
name: Build program
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
args: --bin theseus_cli
|
|
||||||
|
|
||||||
- name: Run Lint
|
|
||||||
uses: actions-rs/clippy-check@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
args: --bin theseus_cli
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -100,6 +100,7 @@ fabric.properties
|
|||||||
# that are supposed to be shared within teams.
|
# that are supposed to be shared within teams.
|
||||||
|
|
||||||
.idea/*
|
.idea/*
|
||||||
|
.vscode/*
|
||||||
|
|
||||||
!.idea/codeStyles
|
!.idea/codeStyles
|
||||||
!.idea/runConfigurations
|
!.idea/runConfigurations
|
||||||
|
|||||||
12
.vscode/extensions.json
vendored
12
.vscode/extensions.json
vendored
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"tauri-apps.tauri-vscode",
|
|
||||||
"vunguyentuan.vscode-css-variables",
|
|
||||||
"stylelint.vscode-stylelint",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"pivaszbs.svelte-autoimport",
|
|
||||||
"svelte.svelte-vscode",
|
|
||||||
"ardenivanov.svelte-intellisense"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
160
.vscode/launch.json
vendored
160
.vscode/launch.json
vendored
@ -1,160 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in library 'theseus'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"--no-run",
|
|
||||||
"--lib",
|
|
||||||
"--package=theseus"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "theseus",
|
|
||||||
"kind": "lib"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug executable 'theseus_cli'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--bin=theseus_cli",
|
|
||||||
"--package=theseus_cli"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "theseus_cli",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in executable 'theseus_cli'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"--no-run",
|
|
||||||
"--bin=theseus_cli",
|
|
||||||
"--package=theseus_cli"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "theseus_cli",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug executable 'theseus_playground'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--bin=theseus_playground",
|
|
||||||
"--package=theseus_playground"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "theseus_playground",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in executable 'theseus_playground'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"--no-run",
|
|
||||||
"--bin=theseus_playground",
|
|
||||||
"--package=theseus_playground"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "theseus_playground",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug executable 'theseus_gui'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--bin=theseus_gui",
|
|
||||||
"--package=theseus_gui"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "theseus_gui",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Debug unit tests in executable 'theseus_gui'",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"test",
|
|
||||||
"--no-run",
|
|
||||||
"--bin=theseus_gui",
|
|
||||||
"--package=theseus_gui"
|
|
||||||
],
|
|
||||||
"filter": {
|
|
||||||
"name": "theseus_gui",
|
|
||||||
"kind": "bin"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Tauri Development Debug",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--manifest-path=./theseus_gui/src-tauri/Cargo.toml",
|
|
||||||
"--no-default-features"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"preLaunchTask": "ui:dev"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Tauri Production Debug",
|
|
||||||
"cargo": {
|
|
||||||
"args": ["build", "--release", "--manifest-path=.theseus_gui/src-tauri/Cargo.toml"]
|
|
||||||
},
|
|
||||||
"preLaunchTask": "ui:build"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
60
.vscode/settings.json
vendored
60
.vscode/settings.json
vendored
@ -1,60 +0,0 @@
|
|||||||
{
|
|
||||||
"cssVariables.lookupFiles": [
|
|
||||||
"**/*.postcss",
|
|
||||||
"**/node_modules/omorphia/**/*.postcss"
|
|
||||||
],
|
|
||||||
"cssVariables.blacklistFolders": [
|
|
||||||
"**/.git",
|
|
||||||
"**/.svn",
|
|
||||||
"**/.hg",
|
|
||||||
"**/CVS",
|
|
||||||
"**/.DS_Store",
|
|
||||||
"**/.git",
|
|
||||||
"**/bower_components",
|
|
||||||
"**/tmp",
|
|
||||||
"**/dist",
|
|
||||||
"**/tests"
|
|
||||||
],
|
|
||||||
"gitlens.showWelcomeOnInstall": false,
|
|
||||||
"gitlens.showWhatsNewAfterUpgrades": false,
|
|
||||||
"gitlens.plusFeatures.enabled": false,
|
|
||||||
"gitlens.currentLine.enabled": false,
|
|
||||||
"gitlens.currentLine.pullRequests.enabled": false,
|
|
||||||
"gitlens.currentLine.scrollable": true,
|
|
||||||
"gitlens.codeLens.enabled": false,
|
|
||||||
"gitlens.hovers.enabled": false,
|
|
||||||
"CSSNavigation.activeCSSFileExtensions": [
|
|
||||||
"css",
|
|
||||||
"postcss"
|
|
||||||
],
|
|
||||||
"CSSNavigation.activeHTMLFileExtensions": [
|
|
||||||
"html",
|
|
||||||
"svelte",
|
|
||||||
"js",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"CSSNavigation.excludeGlobPatterns": [
|
|
||||||
"**/bower_components/**",
|
|
||||||
"**/vendor/**",
|
|
||||||
"**/coverage/**"
|
|
||||||
],
|
|
||||||
"CSSNavigation.alwaysIncludeGlobPatterns": [
|
|
||||||
"./theseus_gui/node_modules/omorphia/**/*.postcss"
|
|
||||||
],
|
|
||||||
"html-css-class-completion.HTMLLanguages": [
|
|
||||||
"html",
|
|
||||||
"svelte"
|
|
||||||
],
|
|
||||||
"html-css-class-completion.includeGlobPattern": "**/*.{postcss,svelte}",
|
|
||||||
"html-css-class-completion.CSSLanguages": [
|
|
||||||
"postcss",
|
|
||||||
],
|
|
||||||
"svelte.enable-ts-plugin": true,
|
|
||||||
"svelte.ask-to-enable-ts-plugin": false,
|
|
||||||
"svelte.plugin.css.diagnostics.enable": false,
|
|
||||||
"svelte.plugin.svelte.diagnostics.enable": false,
|
|
||||||
"rust-analyzer.linkedProjects": [
|
|
||||||
"./theseus/Cargo.toml"
|
|
||||||
],
|
|
||||||
"rust-analyzer.showUnlinkedFileNotification": false,
|
|
||||||
}
|
|
||||||
32
.vscode/tasks.json
vendored
32
.vscode/tasks.json
vendored
@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "ui:dev",
|
|
||||||
"type": "shell",
|
|
||||||
// `dev` keeps running in the background
|
|
||||||
// ideally you should also configure a `problemMatcher`
|
|
||||||
// see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson
|
|
||||||
"isBackground": true,
|
|
||||||
// change this to your `beforeDevCommand`:
|
|
||||||
"command": "yarn",
|
|
||||||
"args": ["dev"],
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/theseus_gui"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "ui:build",
|
|
||||||
"type": "shell",
|
|
||||||
// change this to your `beforeBuildCommand`:
|
|
||||||
"command": "yarn",
|
|
||||||
"args": ["build"],
|
|
||||||
"options": {
|
|
||||||
"cwd": "${workspaceFolder}/theseus_gui"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
453
Cargo.lock
generated
453
Cargo.lock
generated
@ -69,40 +69,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.81"
|
version = "1.0.82"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "argh"
|
|
||||||
version = "0.1.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219"
|
|
||||||
dependencies = [
|
|
||||||
"argh_derive",
|
|
||||||
"argh_shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "argh_derive"
|
|
||||||
version = "0.1.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a"
|
|
||||||
dependencies = [
|
|
||||||
"argh_shared",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.58",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "argh_shared"
|
|
||||||
version = "0.1.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
@ -147,9 +116,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-executor"
|
name = "async-executor"
|
||||||
version = "1.9.1"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "10b3e585719c2358d2660232671ca8ca4ddb4be4ce8a1842d6c2dc8685303316"
|
checksum = "5f98c37cf288e302c16ef6c8472aad1e034c6c84ce5ea7b8101c98eb4a802fee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock 3.3.0",
|
"async-lock 3.3.0",
|
||||||
"async-task",
|
"async-task",
|
||||||
@ -284,9 +253,9 @@ checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.79"
|
version = "0.1.80"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681"
|
checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -376,6 +345,12 @@ dependencies = [
|
|||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base16ct"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@ -504,15 +479,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.15.4"
|
version = "3.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytecount"
|
|
||||||
version = "0.6.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
@ -592,9 +561,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.90"
|
version = "1.0.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
@ -628,9 +597,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-expr"
|
name = "cfg-expr"
|
||||||
version = "0.15.7"
|
version = "0.15.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d"
|
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
@ -719,33 +688,6 @@ dependencies = [
|
|||||||
"objc",
|
"objc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "color-eyre"
|
|
||||||
version = "0.6.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5"
|
|
||||||
dependencies = [
|
|
||||||
"backtrace",
|
|
||||||
"color-spantrace",
|
|
||||||
"eyre",
|
|
||||||
"indenter",
|
|
||||||
"once_cell",
|
|
||||||
"owo-colors",
|
|
||||||
"tracing-error",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "color-spantrace"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"owo-colors",
|
|
||||||
"tracing-core",
|
|
||||||
"tracing-error",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "color_quant"
|
name = "color_quant"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -754,9 +696,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.6"
|
version = "4.6.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
|
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"memchr",
|
"memchr",
|
||||||
@ -784,6 +726,12 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@ -925,6 +873,18 @@ version = "0.8.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-bigint"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@ -1046,6 +1006,17 @@ version = "0.1.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
|
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@ -1080,19 +1051,6 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dialoguer"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
|
||||||
dependencies = [
|
|
||||||
"console",
|
|
||||||
"shell-words",
|
|
||||||
"tempfile",
|
|
||||||
"thiserror",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@ -1100,6 +1058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
|
"const-oid",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
@ -1185,12 +1144,46 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.16.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"digest",
|
||||||
|
"elliptic-curve",
|
||||||
|
"rfc6979",
|
||||||
|
"signature",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.10.0"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elliptic-curve"
|
||||||
|
version = "0.13.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"crypto-bigint",
|
||||||
|
"digest",
|
||||||
|
"ff",
|
||||||
|
"generic-array",
|
||||||
|
"group",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"pkcs8",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"sec1",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embed-resource"
|
name = "embed-resource"
|
||||||
version = "2.4.2"
|
version = "2.4.2"
|
||||||
@ -1219,9 +1212,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.33"
|
version = "0.8.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
|
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
@ -1322,16 +1315,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "eyre"
|
|
||||||
version = "0.6.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
|
||||||
dependencies = [
|
|
||||||
"indenter",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@ -1356,6 +1339,16 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ff"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "field-offset"
|
name = "field-offset"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
@ -1709,6 +1702,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1724,9 +1718,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.13"
|
version = "0.2.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6"
|
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
@ -1855,6 +1849,17 @@ dependencies = [
|
|||||||
"scroll",
|
"scroll",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "group"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||||
|
dependencies = [
|
||||||
|
"ff",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gtk"
|
name = "gtk"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@ -2280,12 +2285,6 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indenter"
|
|
||||||
version = "0.3.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@ -2471,9 +2470,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
version = "0.1.28"
|
version = "0.1.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
|
checksum = "f08474e32172238f2827bd160c67871cdb2801430f65c3979184dc362e3ca118"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@ -3166,10 +3165,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "p256"
|
||||||
version = "3.5.0"
|
version = "0.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||||
|
dependencies = [
|
||||||
|
"ecdsa",
|
||||||
|
"elliptic-curve",
|
||||||
|
"primeorder",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
@ -3196,23 +3201,6 @@ dependencies = [
|
|||||||
"system-deps 6.2.2",
|
"system-deps 6.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "papergrid"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ad43c07024ef767f9160710b3a6773976194758c7919b17e63b863db0bdf7fb"
|
|
||||||
dependencies = [
|
|
||||||
"bytecount",
|
|
||||||
"fnv",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paris"
|
|
||||||
version = "1.5.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8fecab3723493c7851f292cb060f3ee1c42f19b8d749345d0d7eaf3fd19aa62d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@ -3277,6 +3265,15 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem-rfc7468"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
@ -3460,6 +3457,16 @@ dependencies = [
|
|||||||
"futures-io",
|
"futures-io",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
@ -3554,6 +3561,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "primeorder"
|
||||||
|
version = "0.13.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||||
|
dependencies = [
|
||||||
|
"elliptic-curve",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@ -3624,9 +3640,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.35"
|
version = "1.0.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@ -3691,7 +3707,7 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.13",
|
"getrandom 0.2.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3762,7 +3778,7 @@ version = "0.4.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.13",
|
"getrandom 0.2.14",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@ -3879,7 +3895,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls-pemfile 2.1.1",
|
"rustls-pemfile 2.1.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@ -3903,6 +3919,16 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560"
|
checksum = "194d8e591e405d1eecf28819740abed6d719d1a2db87fc0bcdedee9a26d55560"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfc6979"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfd"
|
name = "rfd"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -3980,11 +4006,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "2.1.1"
|
version = "2.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
|
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.22.0",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3996,9 +4022,9 @@ checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.14"
|
version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
@ -4056,6 +4082,20 @@ dependencies = [
|
|||||||
"syn 2.0.58",
|
"syn 2.0.58",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sec1"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"der",
|
||||||
|
"generic-array",
|
||||||
|
"pkcs8",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
@ -4272,9 +4312,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.18"
|
version = "0.1.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -4410,12 +4450,6 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "shell-words"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
@ -4425,6 +4459,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@ -4511,6 +4555,16 @@ dependencies = [
|
|||||||
"system-deps 5.0.0",
|
"system-deps 5.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@ -4623,9 +4677,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sysinfo"
|
name = "sysinfo"
|
||||||
version = "0.30.8"
|
version = "0.30.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b1a378e48fb3ce3a5cf04359c456c9c98ff689bcf1c1bc6e6a31f247686f275"
|
checksum = "26d7c217777061d5a2d652aea771fb9ba98b6dade657204b08c4b9604d11555b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
@ -4676,37 +4730,13 @@ version = "6.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-expr 0.15.7",
|
"cfg-expr 0.15.8",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml 0.8.12",
|
"toml 0.8.12",
|
||||||
"version-compare 0.2.0",
|
"version-compare 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tabled"
|
|
||||||
version = "0.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c998b0c8b921495196a48aabaf1901ff28be0760136e31604f7967b0792050e"
|
|
||||||
dependencies = [
|
|
||||||
"papergrid",
|
|
||||||
"tabled_derive",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tabled_derive"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c138f99377e5d653a371cdad263615634cfc8467685dfe8e73e2b8e98f44b17"
|
|
||||||
dependencies = [
|
|
||||||
"heck 0.4.1",
|
|
||||||
"proc-macro-error",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.16.8"
|
version = "0.16.8"
|
||||||
@ -4918,7 +4948,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-single-instance"
|
name = "tauri-plugin-single-instance"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#cf832fe106cf272916cfdda63235c139dc68171a"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#69ee862fb7f9701f8fca9a5235aeec0eb7714b11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@ -4932,7 +4962,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-window-state"
|
name = "tauri-plugin-window-state"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#cf832fe106cf272916cfdda63235c139dc68171a"
|
source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#69ee862fb7f9701f8fca9a5235aeec0eb7714b11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bincode 1.3.3",
|
"bincode 1.3.3",
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
@ -5054,6 +5084,8 @@ dependencies = [
|
|||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
"async_zip",
|
"async_zip",
|
||||||
|
"base64 0.22.0",
|
||||||
|
"byteorder",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"daedalus",
|
"daedalus",
|
||||||
@ -5066,7 +5098,9 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"notify",
|
"notify",
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
|
"p256",
|
||||||
"paste",
|
"paste",
|
||||||
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest 0.12.3",
|
"reqwest 0.12.3",
|
||||||
"serde",
|
"serde",
|
||||||
@ -5095,33 +5129,6 @@ dependencies = [
|
|||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "theseus_cli"
|
|
||||||
version = "0.6.3"
|
|
||||||
dependencies = [
|
|
||||||
"argh",
|
|
||||||
"color-eyre",
|
|
||||||
"daedalus",
|
|
||||||
"dialoguer",
|
|
||||||
"dirs",
|
|
||||||
"dunce",
|
|
||||||
"eyre",
|
|
||||||
"futures",
|
|
||||||
"paris",
|
|
||||||
"tabled",
|
|
||||||
"theseus",
|
|
||||||
"tokio",
|
|
||||||
"tokio-stream",
|
|
||||||
"tracing",
|
|
||||||
"tracing-error",
|
|
||||||
"tracing-futures",
|
|
||||||
"tracing-subscriber",
|
|
||||||
"url",
|
|
||||||
"uuid 1.8.0",
|
|
||||||
"webbrowser",
|
|
||||||
"winreg 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@ -5223,9 +5230,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.34"
|
version = "0.3.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
|
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa 1.0.11",
|
"itoa 1.0.11",
|
||||||
@ -5244,9 +5251,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.17"
|
version = "0.2.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
|
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
@ -5404,7 +5411,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow 0.6.5",
|
"winnow 0.6.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5490,16 +5497,6 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-futures"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
|
|
||||||
dependencies = [
|
|
||||||
"pin-project",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -5676,7 +5673,7 @@ version = "0.8.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.13",
|
"getrandom 0.2.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -5685,7 +5682,7 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.13",
|
"getrandom 0.2.14",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -5885,9 +5882,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webbrowser"
|
name = "webbrowser"
|
||||||
version = "0.8.14"
|
version = "0.8.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd595fb70f33583ac61644820ebc144a26c96028b625b96cafcd861f4743fbc8"
|
checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"home",
|
"home",
|
||||||
@ -6419,9 +6416,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.5"
|
version = "0.6.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
|
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
members = [
|
members = [
|
||||||
"theseus",
|
"theseus",
|
||||||
"theseus_cli",
|
|
||||||
"theseus_playground",
|
"theseus_playground",
|
||||||
"theseus_gui/src-tauri",
|
"theseus_gui/src-tauri",
|
||||||
"theseus_macros"
|
"theseus_macros"
|
||||||
|
|||||||
11
theseus.iml
11
theseus.iml
@ -1,11 +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$/theseus/library" isTestSource="false" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@ -35,7 +35,7 @@ sysinfo = "0.30.8"
|
|||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["chrono"] }
|
tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] }
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-appender = "0.2.3"
|
tracing-appender = "0.2.3"
|
||||||
|
|
||||||
@ -61,6 +61,11 @@ whoami = "1.4.0"
|
|||||||
|
|
||||||
discord-rich-presence = "0.2.3"
|
discord-rich-presence = "0.2.3"
|
||||||
|
|
||||||
|
p256 = { version = "0.13.2", features = ["ecdsa"] }
|
||||||
|
rand = "0.8"
|
||||||
|
byteorder = "1.5.0"
|
||||||
|
base64 = "0.22.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.52.0"
|
winreg = "0.52.0"
|
||||||
|
|
||||||
|
|||||||
@ -1,132 +0,0 @@
|
|||||||
//! Authentication flow interface
|
|
||||||
use crate::{
|
|
||||||
hydra::{self, init::DeviceLoginSuccess},
|
|
||||||
launcher::auth as inner,
|
|
||||||
State,
|
|
||||||
};
|
|
||||||
use chrono::Utc;
|
|
||||||
|
|
||||||
use crate::state::AuthTask;
|
|
||||||
pub use inner::Credentials;
|
|
||||||
|
|
||||||
/// Authenticate a user with Hydra - part 1
|
|
||||||
/// This begins the authentication flow quasi-synchronously, returning a URL
|
|
||||||
/// This can be used in conjunction with 'authenticate_await_complete_flow'
|
|
||||||
/// to call authenticate and call the flow from the frontend.
|
|
||||||
/// Visit the URL in a browser, then call and await 'authenticate_await_complete_flow'.
|
|
||||||
pub async fn authenticate_begin_flow() -> crate::Result<DeviceLoginSuccess> {
|
|
||||||
let url = AuthTask::begin_auth().await?;
|
|
||||||
Ok(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Authenticate a user with Hydra - part 2
|
|
||||||
/// This completes the authentication flow quasi-synchronously, returning the credentials
|
|
||||||
/// This can be used in conjunction with 'authenticate_begin_flow'
|
|
||||||
/// to call authenticate and call the flow from the frontend.
|
|
||||||
pub async fn authenticate_await_complete_flow() -> crate::Result<Credentials> {
|
|
||||||
let credentials = AuthTask::await_auth_completion().await?;
|
|
||||||
Ok(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cancels the active authentication flow
|
|
||||||
pub async fn cancel_flow() -> crate::Result<()> {
|
|
||||||
AuthTask::cancel().await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Refresh some credentials using Hydra, if needed
|
|
||||||
/// This is the primary desired way to get credentials, as it will also refresh them.
|
|
||||||
#[tracing::instrument]
|
|
||||||
#[theseus_macros::debug_pin]
|
|
||||||
pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let mut users = state.users.write().await;
|
|
||||||
|
|
||||||
let mut credentials = users.get(user).ok_or_else(|| {
|
|
||||||
crate::ErrorKind::OtherError(
|
|
||||||
"You are not logged in with a Minecraft account!".to_string(),
|
|
||||||
)
|
|
||||||
.as_error()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let offline = *state.offline.read().await;
|
|
||||||
|
|
||||||
if !offline {
|
|
||||||
let fetch_semaphore: &crate::util::fetch::FetchSemaphore =
|
|
||||||
&state.fetch_semaphore;
|
|
||||||
if Utc::now() > credentials.expires
|
|
||||||
&& inner::refresh_credentials(&mut credentials, fetch_semaphore)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
users.remove(credentials.id).await?;
|
|
||||||
|
|
||||||
return Err(crate::ErrorKind::OtherError(
|
|
||||||
"Please re-authenticate with your Minecraft account!"
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.as_error());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update player info from bearer token
|
|
||||||
let player_info =
|
|
||||||
hydra::stages::player_info::fetch_info(&credentials.access_token)
|
|
||||||
.await
|
|
||||||
.map_err(|_err| {
|
|
||||||
crate::ErrorKind::HydraError(
|
|
||||||
"No Minecraft account for your profile. Please try again or contact support in our Discord for help!".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
credentials.username = player_info.name;
|
|
||||||
users.insert(&credentials).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a user account from the database
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let mut users = state.users.write().await;
|
|
||||||
|
|
||||||
if state.settings.read().await.default_user == Some(user) {
|
|
||||||
let mut settings = state.settings.write().await;
|
|
||||||
settings.default_user = users.0.values().next().map(|it| it.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
users.remove(user).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a user exists in Theseus
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn has_user(user: uuid::Uuid) -> crate::Result<bool> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let users = state.users.read().await;
|
|
||||||
|
|
||||||
Ok(users.contains(user))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a copy of the list of all user credentials
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn users() -> crate::Result<Vec<Credentials>> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let users = state.users.read().await;
|
|
||||||
Ok(users.0.values().cloned().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a specific user by user ID
|
|
||||||
/// Prefer to use 'refresh' instead of this function
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let users = state.users.read().await;
|
|
||||||
let user = users.get(user).ok_or_else(|| {
|
|
||||||
crate::ErrorKind::OtherError(format!(
|
|
||||||
"Tried to get nonexistent user with ID {user}"
|
|
||||||
))
|
|
||||||
.as_error()
|
|
||||||
})?;
|
|
||||||
Ok(user)
|
|
||||||
}
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
//! Main authentication flow for Hydra
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::prelude::Credentials;
|
|
||||||
|
|
||||||
use super::stages::{
|
|
||||||
bearer_token, player_info, poll_response, xbl_signin, xsts_token,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct OauthFailure {
|
|
||||||
pub error: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SuccessfulLogin {
|
|
||||||
pub name: String,
|
|
||||||
pub icon: String,
|
|
||||||
pub token: String,
|
|
||||||
pub refresh_token: String,
|
|
||||||
pub expires_after: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wait_finish(device_code: String) -> crate::Result<Credentials> {
|
|
||||||
// Loop, polling for response from Microsoft
|
|
||||||
let oauth = poll_response::poll_response(device_code).await?;
|
|
||||||
|
|
||||||
// Get xbl token from oauth token
|
|
||||||
let xbl_token = xbl_signin::login_xbl(&oauth.access_token).await?;
|
|
||||||
|
|
||||||
// Get xsts token from xbl token
|
|
||||||
let xsts_response = xsts_token::fetch_token(&xbl_token.token).await?;
|
|
||||||
|
|
||||||
match xsts_response {
|
|
||||||
xsts_token::XSTSResponse::Unauthorized(err) => {
|
|
||||||
Err(crate::ErrorKind::HydraError(format!(
|
|
||||||
"Error getting XBox Live token: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
.as_error())
|
|
||||||
}
|
|
||||||
xsts_token::XSTSResponse::Success { token: xsts_token } => {
|
|
||||||
// Get xsts bearer token from xsts token
|
|
||||||
let (bearer_token, expires_in) =
|
|
||||||
bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs)
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
crate::ErrorKind::HydraError(format!(
|
|
||||||
"Error getting bearer token: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get player info from bearer token
|
|
||||||
let player_info = player_info::fetch_info(&bearer_token).await.map_err(|_err| {
|
|
||||||
crate::ErrorKind::HydraError("No Minecraft account for profile. Make sure you own the game and have set a username through the official Minecraft launcher."
|
|
||||||
.to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Create credentials
|
|
||||||
let credentials = Credentials::new(
|
|
||||||
uuid::Uuid::parse_str(&player_info.id)?, // get uuid from player_info.id which is a String
|
|
||||||
player_info.name,
|
|
||||||
bearer_token,
|
|
||||||
oauth.refresh_token,
|
|
||||||
chrono::Utc::now() + chrono::Duration::seconds(expires_in),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Put credentials into state
|
|
||||||
let state = crate::State::get().await?;
|
|
||||||
{
|
|
||||||
let mut users = state.users.write().await;
|
|
||||||
users.insert(&credentials).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.settings.read().await.default_user.is_none() {
|
|
||||||
let mut settings = state.settings.write().await;
|
|
||||||
settings.default_user = Some(credentials.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(credentials)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
//! Login route for Hydra, redirects to the Microsoft login page before going to the redirect route
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{hydra::MicrosoftError, util::fetch::REQWEST_CLIENT};
|
|
||||||
|
|
||||||
use super::{stages::auth_retry, MICROSOFT_CLIENT_ID};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct DeviceLoginSuccess {
|
|
||||||
pub device_code: String,
|
|
||||||
pub user_code: String,
|
|
||||||
pub verification_uri: String,
|
|
||||||
pub expires_in: u64,
|
|
||||||
pub interval: u64,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init() -> crate::Result<DeviceLoginSuccess> {
|
|
||||||
// Get the initial URL
|
|
||||||
// Get device code
|
|
||||||
// Define the parameters
|
|
||||||
|
|
||||||
// urlencoding::encode("XboxLive.signin offline_access"));
|
|
||||||
let resp = auth_retry(|| REQWEST_CLIENT.get("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
|
|
||||||
.header("Content-Length", "0")
|
|
||||||
.query(&[
|
|
||||||
("client_id", MICROSOFT_CLIENT_ID),
|
|
||||||
(
|
|
||||||
"scope",
|
|
||||||
"XboxLive.signin XboxLive.offline_access profile openid email",
|
|
||||||
),
|
|
||||||
])
|
|
||||||
.send()
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
match resp.status() {
|
|
||||||
reqwest::StatusCode::OK => Ok(resp.json().await?),
|
|
||||||
_ => {
|
|
||||||
let microsoft_error = resp.json::<MicrosoftError>().await?;
|
|
||||||
Err(crate::ErrorKind::HydraError(format!(
|
|
||||||
"Error from Microsoft: {:?}",
|
|
||||||
microsoft_error.error_description
|
|
||||||
))
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
pub mod complete;
|
|
||||||
pub mod init;
|
|
||||||
pub mod refresh;
|
|
||||||
pub(crate) mod stages;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
const MICROSOFT_CLIENT_ID: &str = "c4502edb-87c6-40cb-b595-64a280cf8906";
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct MicrosoftError {
|
|
||||||
pub error: String,
|
|
||||||
pub error_description: String,
|
|
||||||
pub error_codes: Vec<u64>,
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use reqwest::StatusCode;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
|
|
||||||
util::fetch::REQWEST_CLIENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::stages::auth_retry;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct OauthSuccess {
|
|
||||||
pub token_type: String,
|
|
||||||
pub scope: String,
|
|
||||||
pub expires_in: i64,
|
|
||||||
pub access_token: String,
|
|
||||||
pub refresh_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh(refresh_token: String) -> crate::Result<OauthSuccess> {
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
params.insert("grant_type", "refresh_token");
|
|
||||||
params.insert("client_id", MICROSOFT_CLIENT_ID);
|
|
||||||
params.insert("refresh_token", &refresh_token);
|
|
||||||
params.insert(
|
|
||||||
"redirect_uri",
|
|
||||||
"https://login.microsoftonline.com/common/oauth2/nativeclient",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Poll the URL in a loop until we are successful.
|
|
||||||
// On an authorization_pending response, wait 5 seconds and try again.
|
|
||||||
let resp =
|
|
||||||
auth_retry(|| {
|
|
||||||
REQWEST_CLIENT
|
|
||||||
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
|
|
||||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match resp.status() {
|
|
||||||
StatusCode::OK => {
|
|
||||||
let oauth = resp.json::<OauthSuccess>().await.map_err(|err| {
|
|
||||||
crate::ErrorKind::HydraError(format!(
|
|
||||||
"Could not decipher successful response: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
Ok(oauth)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let failure =
|
|
||||||
resp.json::<MicrosoftError>().await.map_err(|err| {
|
|
||||||
crate::ErrorKind::HydraError(format!(
|
|
||||||
"Could not decipher failure response: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
Err(crate::ErrorKind::HydraError(format!(
|
|
||||||
"Error refreshing token: {}",
|
|
||||||
failure.error
|
|
||||||
))
|
|
||||||
.as_error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use super::auth_retry;
|
|
||||||
|
|
||||||
const MCSERVICES_AUTH_URL: &str =
|
|
||||||
"https://api.minecraftservices.com/authentication/login_with_xbox";
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct BearerTokenResponse {
|
|
||||||
access_token: String,
|
|
||||||
expires_in: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn fetch_bearer(
|
|
||||||
token: &str,
|
|
||||||
uhs: &str,
|
|
||||||
) -> crate::Result<(String, i64)> {
|
|
||||||
let body = auth_retry(|| {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
client
|
|
||||||
.post(MCSERVICES_AUTH_URL)
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.json(&json!({
|
|
||||||
"identityToken": format!("XBL3.0 x={};{}", uhs, token),
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
serde_json::from_str::<BearerTokenResponse>(&body)
|
|
||||||
.map(|x| (x.access_token, x.expires_in))
|
|
||||||
.map_err(|_| {
|
|
||||||
crate::ErrorKind::HydraError(format!(
|
|
||||||
"Response didn't contain valid bearer token. body: {body}"
|
|
||||||
))
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
//! MSA authentication stages
|
|
||||||
|
|
||||||
use futures::Future;
|
|
||||||
use reqwest::Response;
|
|
||||||
|
|
||||||
const RETRY_COUNT: usize = 9; // Does command 3 times
|
|
||||||
const RETRY_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
|
|
||||||
|
|
||||||
pub mod bearer_token;
|
|
||||||
pub mod player_info;
|
|
||||||
pub mod poll_response;
|
|
||||||
pub mod xbl_signin;
|
|
||||||
pub mod xsts_token;
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(reqwest_request))]
|
|
||||||
pub async fn auth_retry<F>(
|
|
||||||
reqwest_request: impl Fn() -> F,
|
|
||||||
) -> crate::Result<reqwest::Response>
|
|
||||||
where
|
|
||||||
F: Future<Output = Result<Response, reqwest::Error>>,
|
|
||||||
{
|
|
||||||
let mut resp = reqwest_request().await?;
|
|
||||||
for i in 0..RETRY_COUNT {
|
|
||||||
if resp.status().is_success() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tracing::debug!(
|
|
||||||
"Request failed with status code {}, retrying...",
|
|
||||||
resp.status()
|
|
||||||
);
|
|
||||||
if i < RETRY_COUNT - 1 {
|
|
||||||
tokio::time::sleep(RETRY_WAIT).await;
|
|
||||||
}
|
|
||||||
resp = reqwest_request().await?;
|
|
||||||
}
|
|
||||||
Ok(resp)
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
//! Fetch player info for display
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::util::fetch::REQWEST_CLIENT;
|
|
||||||
|
|
||||||
use super::auth_retry;
|
|
||||||
|
|
||||||
const PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct PlayerInfo {
|
|
||||||
pub id: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PlayerInfo {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: "606e2ff0ed7748429d6ce1d3321c7838".to_string(),
|
|
||||||
name: String::from("Unknown"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn fetch_info(token: &str) -> crate::Result<PlayerInfo> {
|
|
||||||
auth_retry(|| {
|
|
||||||
REQWEST_CLIENT
|
|
||||||
.get("https://api.minecraftservices.com/entitlements/mcstore")
|
|
||||||
.bearer_auth(token)
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let response = auth_retry(|| {
|
|
||||||
REQWEST_CLIENT
|
|
||||||
.get(PROFILE_URL)
|
|
||||||
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let resp = response.error_for_status()?.json().await?;
|
|
||||||
|
|
||||||
Ok(resp)
|
|
||||||
}
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use reqwest::StatusCode;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
|
|
||||||
util::fetch::REQWEST_CLIENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::auth_retry;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct OauthSuccess {
|
|
||||||
pub token_type: String,
|
|
||||||
pub scope: String,
|
|
||||||
pub expires_in: i64,
|
|
||||||
pub access_token: String,
|
|
||||||
pub refresh_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn poll_response(device_code: String) -> crate::Result<OauthSuccess> {
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
||||||
params.insert("client_id", MICROSOFT_CLIENT_ID);
|
|
||||||
params.insert("device_code", &device_code);
|
|
||||||
params.insert(
|
|
||||||
"scope",
|
|
||||||
"XboxLive.signin XboxLive.offline_access profile openid email",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Poll the URL in a loop until we are successful.
|
|
||||||
// On an authorization_pending response, wait 5 seconds and try again.
|
|
||||||
loop {
|
|
||||||
let resp = auth_retry(|| {
|
|
||||||
REQWEST_CLIENT
|
|
||||||
.post(
|
|
||||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
|
|
||||||
)
|
|
||||||
.form(¶ms)
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match resp.status() {
|
|
||||||
StatusCode::OK => {
|
|
||||||
let oauth =
|
|
||||||
resp.json::<OauthSuccess>().await.map_err(|err| {
|
|
||||||
crate::ErrorKind::HydraError(format!(
|
|
||||||
"Could not decipher successful response: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
return Ok(oauth);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let failure =
|
|
||||||
resp.json::<MicrosoftError>().await.map_err(|err| {
|
|
||||||
crate::ErrorKind::HydraError(format!(
|
|
||||||
"Could not decipher failure response: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
match failure.error.as_str() {
|
|
||||||
"authorization_pending" => {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(2))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
"authorization_declined" => {
|
|
||||||
return Err(crate::ErrorKind::HydraError(
|
|
||||||
"Authorization declined".to_string(),
|
|
||||||
)
|
|
||||||
.as_error());
|
|
||||||
}
|
|
||||||
"expired_token" => {
|
|
||||||
return Err(crate::ErrorKind::HydraError(
|
|
||||||
"Device code expired".to_string(),
|
|
||||||
)
|
|
||||||
.as_error());
|
|
||||||
}
|
|
||||||
"bad_verification_code" => {
|
|
||||||
return Err(crate::ErrorKind::HydraError(
|
|
||||||
"Invalid device code".to_string(),
|
|
||||||
)
|
|
||||||
.as_error());
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(crate::ErrorKind::HydraError(format!(
|
|
||||||
"Unknown error: {}",
|
|
||||||
failure.error
|
|
||||||
))
|
|
||||||
.as_error());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::util::fetch::REQWEST_CLIENT;
|
|
||||||
|
|
||||||
use super::auth_retry;
|
|
||||||
|
|
||||||
const XBL_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
|
|
||||||
|
|
||||||
// Deserialization
|
|
||||||
pub struct XBLLogin {
|
|
||||||
pub token: String,
|
|
||||||
pub uhs: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Impl
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn login_xbl(token: &str) -> crate::Result<XBLLogin> {
|
|
||||||
let response = auth_retry(|| {
|
|
||||||
REQWEST_CLIENT
|
|
||||||
.post(XBL_AUTH_URL)
|
|
||||||
.header(reqwest::header::ACCEPT, "application/json")
|
|
||||||
.json(&json!({
|
|
||||||
"Properties": {
|
|
||||||
"AuthMethod": "RPS",
|
|
||||||
"SiteName": "user.auth.xboxlive.com",
|
|
||||||
"RpsTicket": format!("d={token}")
|
|
||||||
},
|
|
||||||
"RelyingParty": "http://auth.xboxlive.com",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let body = response.text().await?;
|
|
||||||
|
|
||||||
let json = serde_json::from_str::<serde_json::Value>(&body)?;
|
|
||||||
let token = Some(&json)
|
|
||||||
.and_then(|it| it.get("Token")?.as_str().map(String::from))
|
|
||||||
.ok_or(crate::ErrorKind::HydraError(
|
|
||||||
"XBL response didn't contain valid token".to_string(),
|
|
||||||
))?;
|
|
||||||
let uhs = Some(&json)
|
|
||||||
.and_then(|it| {
|
|
||||||
it.get("DisplayClaims")?
|
|
||||||
.get("xui")?
|
|
||||||
.get(0)?
|
|
||||||
.get("uhs")?
|
|
||||||
.as_str()
|
|
||||||
.map(String::from)
|
|
||||||
})
|
|
||||||
.ok_or(
|
|
||||||
crate::ErrorKind::HydraError(
|
|
||||||
"XBL response didn't contain valid user hash".to_string(),
|
|
||||||
)
|
|
||||||
.as_error(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(XBLLogin { token, uhs })
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::util::fetch::REQWEST_CLIENT;
|
|
||||||
|
|
||||||
use super::auth_retry;
|
|
||||||
|
|
||||||
const XSTS_AUTH_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
|
|
||||||
|
|
||||||
pub enum XSTSResponse {
|
|
||||||
Unauthorized(String),
|
|
||||||
Success { token: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn fetch_token(token: &str) -> crate::Result<XSTSResponse> {
|
|
||||||
let resp = auth_retry(|| {
|
|
||||||
REQWEST_CLIENT
|
|
||||||
.post(XSTS_AUTH_URL)
|
|
||||||
.header(reqwest::header::ACCEPT, "application/json")
|
|
||||||
.json(&json!({
|
|
||||||
"Properties": {
|
|
||||||
"SandboxId": "RETAIL",
|
|
||||||
"UserTokens": [
|
|
||||||
token
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
|
||||||
"TokenType": "JWT"
|
|
||||||
}))
|
|
||||||
.send()
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let status = resp.status();
|
|
||||||
|
|
||||||
let body = resp.text().await?;
|
|
||||||
let json = serde_json::from_str::<serde_json::Value>(&body)?;
|
|
||||||
|
|
||||||
if status.is_success() {
|
|
||||||
Ok(json
|
|
||||||
.get("Token")
|
|
||||||
.and_then(|x| x.as_str().map(String::from))
|
|
||||||
.map(|it| XSTSResponse::Success { token: it })
|
|
||||||
.unwrap_or(XSTSResponse::Unauthorized(
|
|
||||||
"XSTS response didn't contain valid token!".to_string(),
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
Ok(XSTSResponse::Unauthorized(
|
|
||||||
#[allow(clippy::unreadable_literal)]
|
|
||||||
match json.get("XErr").and_then(|x| x.as_i64()) {
|
|
||||||
Some(2148916238) => {
|
|
||||||
String::from("This Microsoft account is underage and is not linked to a family.")
|
|
||||||
},
|
|
||||||
Some(2148916235) => {
|
|
||||||
String::from("XBOX Live/Minecraft is not available in your country.")
|
|
||||||
},
|
|
||||||
Some(2148916233) => String::from("This account does not have a valid XBOX Live profile. Please buy Minecraft and try again!"),
|
|
||||||
Some(2148916236) | Some(2148916237) => String::from("This account needs adult verification on Xbox page."),
|
|
||||||
_ => String::from("Unknown error code"),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -140,8 +140,14 @@ pub async fn get_output_by_filename(
|
|||||||
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
|
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
|
||||||
let path = logs_folder.join(file_name);
|
let path = logs_folder.join(file_name);
|
||||||
|
|
||||||
let credentials: Vec<Credentials> =
|
let credentials: Vec<Credentials> = state
|
||||||
state.users.read().await.clone().0.into_values().collect();
|
.users
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.users
|
||||||
|
.clone()
|
||||||
|
.into_values()
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Load .gz file into String
|
// Load .gz file into String
|
||||||
if let Some(ext) = path.extension() {
|
if let Some(ext) = path.extension() {
|
||||||
@ -296,8 +302,14 @@ pub async fn get_generic_live_log_cursor(
|
|||||||
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
|
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
|
||||||
let cursor = cursor + bytes_read as u64; // Update cursor
|
let cursor = cursor + bytes_read as u64; // Update cursor
|
||||||
|
|
||||||
let credentials: Vec<Credentials> =
|
let credentials: Vec<Credentials> = state
|
||||||
state.users.read().await.clone().0.into_values().collect();
|
.users
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.users
|
||||||
|
.clone()
|
||||||
|
.into_values()
|
||||||
|
.collect();
|
||||||
let output = CensoredString::censor(output, &credentials);
|
let output = CensoredString::censor(output, &credentials);
|
||||||
Ok(LatestLogCursor {
|
Ok(LatestLogCursor {
|
||||||
cursor,
|
cursor,
|
||||||
|
|||||||
76
theseus/src/api/minecraft_auth.rs
Normal file
76
theseus/src/api/minecraft_auth.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
//! Authentication flow interface
|
||||||
|
use crate::state::{Credentials, MinecraftLoginFlow};
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut users = state.users.write().await;
|
||||||
|
|
||||||
|
users.login_begin().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn finish_login(
|
||||||
|
code: &str,
|
||||||
|
flow: MinecraftLoginFlow,
|
||||||
|
) -> crate::Result<Credentials> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut users = state.users.write().await;
|
||||||
|
|
||||||
|
users.login_finish(code, flow).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let users = state.users.read().await;
|
||||||
|
Ok(users.default_user)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||||
|
let user = get_user(user).await?;
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut users = state.users.write().await;
|
||||||
|
users.default_user = Some(user.id);
|
||||||
|
users.save().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a user account from the database
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut users = state.users.write().await;
|
||||||
|
users.remove(user).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a copy of the list of all user credentials
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn users() -> crate::Result<Vec<Credentials>> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let users = state.users.read().await;
|
||||||
|
Ok(users.users.values().cloned().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific user by user ID
|
||||||
|
/// Prefer to use 'refresh' instead of this function
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let users = state.users.read().await;
|
||||||
|
let user = users
|
||||||
|
.users
|
||||||
|
.get(&user)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::OtherError(format!(
|
||||||
|
"Tried to get nonexistent user with ID {user}"
|
||||||
|
))
|
||||||
|
.as_error()
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
@ -1,10 +1,9 @@
|
|||||||
//! API for interacting with Theseus
|
//! API for interacting with Theseus
|
||||||
pub mod auth;
|
|
||||||
pub mod handler;
|
pub mod handler;
|
||||||
pub mod hydra;
|
|
||||||
pub mod jre;
|
pub mod jre;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
pub mod minecraft_auth;
|
||||||
pub mod mr_auth;
|
pub mod mr_auth;
|
||||||
pub mod pack;
|
pub mod pack;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
@ -15,19 +14,19 @@ pub mod tags;
|
|||||||
|
|
||||||
pub mod data {
|
pub mod data {
|
||||||
pub use crate::state::{
|
pub use crate::state::{
|
||||||
DirectoryInfo, Hooks, JavaSettings, LinkedData, MemorySettings,
|
Credentials, DirectoryInfo, Hooks, JavaSettings, LinkedData,
|
||||||
ModLoader, ModrinthCredentials, ModrinthCredentialsResult,
|
MemorySettings, ModLoader, ModrinthCredentials,
|
||||||
ModrinthProject, ModrinthTeamMember, ModrinthUser, ModrinthVersion,
|
ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember,
|
||||||
ProfileMetadata, ProjectMetadata, Settings, Theme, WindowSize,
|
ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata,
|
||||||
|
Settings, Theme, WindowSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
auth::{self, Credentials},
|
|
||||||
data::*,
|
data::*,
|
||||||
event::CommandPayload,
|
event::CommandPayload,
|
||||||
jre, metadata, pack, process,
|
jre, metadata, minecraft_auth, pack, process,
|
||||||
profile::{self, create, Profile},
|
profile::{self, create, Profile},
|
||||||
settings,
|
settings,
|
||||||
state::JavaGlobals,
|
state::JavaGlobals,
|
||||||
|
|||||||
@ -8,12 +8,13 @@ use crate::pack::install_from::{
|
|||||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||||
};
|
};
|
||||||
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
||||||
use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType};
|
use crate::state::{
|
||||||
|
Credentials, InnerProjectPathUnix, ProjectMetadata, SideType,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::util::fetch;
|
use crate::util::fetch;
|
||||||
use crate::util::io::{self, IOError};
|
use crate::util::io::{self, IOError};
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{self, refresh},
|
|
||||||
event::{emit::emit_profile, ProfilePayloadType},
|
event::{emit::emit_profile, ProfilePayloadType},
|
||||||
state::MinecraftChild,
|
state::MinecraftChild,
|
||||||
};
|
};
|
||||||
@ -745,20 +746,16 @@ pub async fn run(
|
|||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
|
||||||
// Get default account and refresh credentials (preferred way to log in)
|
// Get default account and refresh credentials (preferred way to log in)
|
||||||
let default_account = state.settings.read().await.default_user;
|
let default_account = {
|
||||||
let credentials = if let Some(default_account) = default_account {
|
let mut write = state.users.write().await;
|
||||||
refresh(default_account).await?
|
|
||||||
} else {
|
write
|
||||||
// If no default account, try to use a logged in account
|
.get_default_credential()
|
||||||
let users = auth::users().await?;
|
.await?
|
||||||
let last_account = users.first();
|
.ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?
|
||||||
if let Some(last_account) = last_account {
|
|
||||||
refresh(last_account.id).await?
|
|
||||||
} else {
|
|
||||||
return Err(crate::ErrorKind::NoCredentialsError.as_error());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
run_credentials(path, &credentials).await
|
|
||||||
|
run_credentials(path, &default_account).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run Minecraft using a profile, and credentials for authentication
|
/// Run Minecraft using a profile, and credentials for authentication
|
||||||
@ -767,7 +764,7 @@ pub async fn run(
|
|||||||
#[theseus_macros::debug_pin]
|
#[theseus_macros::debug_pin]
|
||||||
pub async fn run_credentials(
|
pub async fn run_credentials(
|
||||||
path: &ProfilePathId,
|
path: &ProfilePathId,
|
||||||
credentials: &auth::Credentials,
|
credentials: &Credentials,
|
||||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let settings = state.settings.read().await;
|
let settings = state.settings.read().await;
|
||||||
|
|||||||
@ -25,11 +25,10 @@ pub enum ErrorKind {
|
|||||||
#[error("Metadata error: {0}")]
|
#[error("Metadata error: {0}")]
|
||||||
MetadataError(#[from] daedalus::Error),
|
MetadataError(#[from] daedalus::Error),
|
||||||
|
|
||||||
#[error("Minecraft authentication Hydra error: {0}")]
|
#[error("Minecraft authentication error: {0}")]
|
||||||
HydraError(String),
|
MinecraftAuthenticationError(
|
||||||
|
#[from] crate::state::MinecraftAuthenticationError,
|
||||||
#[error("Minecraft authentication task error: {0}")]
|
),
|
||||||
AuthTaskError(#[from] crate::state::AuthTaskError),
|
|
||||||
|
|
||||||
#[error("I/O error: {0}")]
|
#[error("I/O error: {0}")]
|
||||||
IOError(#[from] util::io::IOError),
|
IOError(#[from] util::io::IOError),
|
||||||
|
|||||||
@ -43,7 +43,7 @@ impl EventState {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map(Arc::clone)
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
//! Minecraft CLI argument logic
|
//! Minecraft CLI argument logic
|
||||||
use super::auth::Credentials;
|
|
||||||
use crate::launcher::parse_rules;
|
use crate::launcher::parse_rules;
|
||||||
|
use crate::state::Credentials;
|
||||||
use crate::{
|
use crate::{
|
||||||
state::{MemorySettings, WindowSize},
|
state::{MemorySettings, WindowSize},
|
||||||
util::{io::IOError, platform::classpath_separator},
|
util::{io::IOError, platform::classpath_separator},
|
||||||
|
|||||||
@ -1,84 +0,0 @@
|
|||||||
//! Authentication flow based on Hydra
|
|
||||||
|
|
||||||
use crate::hydra;
|
|
||||||
use crate::util::fetch::FetchSemaphore;
|
|
||||||
|
|
||||||
use chrono::{prelude::*, Duration};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::api::hydra::stages::{bearer_token, xbl_signin, xsts_token};
|
|
||||||
|
|
||||||
// Login information
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct Credentials {
|
|
||||||
pub id: uuid::Uuid,
|
|
||||||
pub username: String,
|
|
||||||
pub access_token: String,
|
|
||||||
pub refresh_token: String,
|
|
||||||
pub expires: DateTime<Utc>,
|
|
||||||
_ctor_scope: std::marker::PhantomData<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Credentials {
|
|
||||||
pub fn new(
|
|
||||||
id: uuid::Uuid,
|
|
||||||
username: String,
|
|
||||||
access_token: String,
|
|
||||||
refresh_token: String,
|
|
||||||
expires: DateTime<Utc>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
access_token,
|
|
||||||
refresh_token,
|
|
||||||
expires,
|
|
||||||
_ctor_scope: std::marker::PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_expired(&self) -> bool {
|
|
||||||
self.expires < Utc::now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_credentials(
|
|
||||||
credentials: &mut Credentials,
|
|
||||||
_semaphore: &FetchSemaphore,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
let oauth =
|
|
||||||
hydra::refresh::refresh(credentials.refresh_token.clone()).await?;
|
|
||||||
|
|
||||||
let xbl_token = xbl_signin::login_xbl(&oauth.access_token).await?;
|
|
||||||
|
|
||||||
// Get xsts token from xbl token
|
|
||||||
let xsts_response = xsts_token::fetch_token(&xbl_token.token).await?;
|
|
||||||
|
|
||||||
match xsts_response {
|
|
||||||
xsts_token::XSTSResponse::Unauthorized(err) => {
|
|
||||||
return Err(crate::ErrorKind::HydraError(format!(
|
|
||||||
"Error getting XBox Live token: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
.as_error())
|
|
||||||
}
|
|
||||||
xsts_token::XSTSResponse::Success { token: xsts_token } => {
|
|
||||||
let (bearer_token, expires_in) =
|
|
||||||
bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs)
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
crate::ErrorKind::HydraError(format!(
|
|
||||||
"Error getting bearer token: {}",
|
|
||||||
err
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
credentials.access_token = bearer_token;
|
|
||||||
credentials.refresh_token = oauth.refresh_token;
|
|
||||||
credentials.expires = Utc::now() + Duration::seconds(expires_in);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ use crate::event::{LoadingBarId, LoadingBarType};
|
|||||||
use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
|
use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
|
||||||
use crate::launcher::io::IOError;
|
use crate::launcher::io::IOError;
|
||||||
use crate::prelude::JavaVersion;
|
use crate::prelude::JavaVersion;
|
||||||
use crate::state::ProfileInstallStage;
|
use crate::state::{Credentials, ProfileInstallStage};
|
||||||
use crate::util::io;
|
use crate::util::io;
|
||||||
use crate::{
|
use crate::{
|
||||||
process,
|
process,
|
||||||
@ -22,7 +22,6 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
|
|
||||||
pub mod auth;
|
|
||||||
pub mod download;
|
pub mod download;
|
||||||
|
|
||||||
// All nones -> disallowed
|
// All nones -> disallowed
|
||||||
@ -368,7 +367,7 @@ pub async fn launch_minecraft(
|
|||||||
wrapper: &Option<String>,
|
wrapper: &Option<String>,
|
||||||
memory: &st::MemorySettings,
|
memory: &st::MemorySettings,
|
||||||
resolution: &st::WindowSize,
|
resolution: &st::WindowSize,
|
||||||
credentials: &auth::Credentials,
|
credentials: &Credentials,
|
||||||
post_exit_hook: Option<String>,
|
post_exit_hook: Option<String>,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {
|
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
hydra::{self, init::DeviceLoginSuccess},
|
|
||||||
launcher::auth::Credentials,
|
|
||||||
};
|
|
||||||
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
|
|
||||||
// Authentication task
|
|
||||||
// A wrapper over the authentication task that allows it to be called from the frontend
|
|
||||||
// without caching the task handle in the frontend
|
|
||||||
|
|
||||||
pub struct AuthTask(
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
Option<JoinHandle<crate::Result<Credentials>>>,
|
|
||||||
);
|
|
||||||
|
|
||||||
impl AuthTask {
|
|
||||||
pub fn new() -> AuthTask {
|
|
||||||
AuthTask(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn begin_auth() -> crate::Result<DeviceLoginSuccess> {
|
|
||||||
let state = crate::State::get().await?;
|
|
||||||
// Init task, get url
|
|
||||||
let login = hydra::init::init().await?;
|
|
||||||
|
|
||||||
// Await completion
|
|
||||||
let task = tokio::spawn(hydra::complete::wait_finish(
|
|
||||||
login.device_code.clone(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Flow is going, store in state and return
|
|
||||||
let mut write = state.auth_flow.write().await;
|
|
||||||
write.0 = Some(task);
|
|
||||||
|
|
||||||
Ok(login)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn await_auth_completion() -> crate::Result<Credentials> {
|
|
||||||
// Gets the task handle from the state, replacing with None
|
|
||||||
let task = {
|
|
||||||
let state = crate::State::get().await?;
|
|
||||||
let mut write = state.auth_flow.write().await;
|
|
||||||
|
|
||||||
write.0.take()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Waits for the task to complete, and returns the credentials
|
|
||||||
let credentials = task
|
|
||||||
.ok_or(AuthTaskError::TaskMissing)?
|
|
||||||
.await
|
|
||||||
.map_err(AuthTaskError::from)??;
|
|
||||||
|
|
||||||
Ok(credentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn cancel() -> crate::Result<()> {
|
|
||||||
// Gets the task handle from the state, replacing with None
|
|
||||||
let task = {
|
|
||||||
let state = crate::State::get().await?;
|
|
||||||
let mut write = state.auth_flow.write().await;
|
|
||||||
|
|
||||||
write.0.take()
|
|
||||||
};
|
|
||||||
if let Some(task) = task {
|
|
||||||
// Cancels the task
|
|
||||||
task.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AuthTask {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum AuthTaskError {
|
|
||||||
#[error("Authentication task was aborted or missing")]
|
|
||||||
TaskMissing,
|
|
||||||
#[error("Join handle error")]
|
|
||||||
JoinHandleError(#[from] tokio::task::JoinError),
|
|
||||||
}
|
|
||||||
878
theseus/src/state/minecraft_auth.rs
Normal file
878
theseus/src/state/minecraft_auth.rs
Normal file
@ -0,0 +1,878 @@
|
|||||||
|
use crate::data::DirectoryInfo;
|
||||||
|
use crate::util::fetch::{read_json, write, IoSemaphore, REQWEST_CLIENT};
|
||||||
|
use crate::State;
|
||||||
|
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
|
||||||
|
use base64::Engine;
|
||||||
|
use byteorder::BigEndian;
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use p256::ecdsa::signature::Signer;
|
||||||
|
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
|
||||||
|
use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::Rng;
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use reqwest::Response;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum MinecraftAuthStep {
|
||||||
|
GetDeviceToken,
|
||||||
|
SisuAuthenicate,
|
||||||
|
GetOAuthToken,
|
||||||
|
RefreshOAuthToken,
|
||||||
|
SisuAuthorize,
|
||||||
|
XstsAuthorize,
|
||||||
|
MinecraftToken,
|
||||||
|
MinecraftEntitlements,
|
||||||
|
MinecraftProfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum MinecraftAuthenticationError {
|
||||||
|
#[error("Failed to serialize private key to PEM: {0}")]
|
||||||
|
PEMSerialize(#[from] p256::pkcs8::Error),
|
||||||
|
#[error("Failed to serialize body to JSON during step {step:?}: {source}")]
|
||||||
|
SerializeBody {
|
||||||
|
step: MinecraftAuthStep,
|
||||||
|
#[source]
|
||||||
|
source: serde_json::Error,
|
||||||
|
},
|
||||||
|
#[error(
|
||||||
|
"Failed to deserialize response to JSON during step {step:?}: {source}"
|
||||||
|
)]
|
||||||
|
DeserializeResponse {
|
||||||
|
step: MinecraftAuthStep,
|
||||||
|
raw: String,
|
||||||
|
#[source]
|
||||||
|
source: serde_json::Error,
|
||||||
|
},
|
||||||
|
#[error("Request failed during step {step:?}: {source}")]
|
||||||
|
Request {
|
||||||
|
step: MinecraftAuthStep,
|
||||||
|
#[source]
|
||||||
|
source: reqwest::Error,
|
||||||
|
},
|
||||||
|
#[error("Error creating signed request buffer {step:?}: {source}")]
|
||||||
|
ConstructingSignedRequest {
|
||||||
|
step: MinecraftAuthStep,
|
||||||
|
#[source]
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
#[error("Error reading user hash")]
|
||||||
|
NoUserHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AUTH_JSON: &str = "minecraft_auth.json";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SaveDeviceToken {
|
||||||
|
pub id: String,
|
||||||
|
pub private_key: String,
|
||||||
|
pub x: String,
|
||||||
|
pub y: String,
|
||||||
|
pub token: DeviceToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct MinecraftLoginFlow {
|
||||||
|
pub challenge: String,
|
||||||
|
pub session_id: String,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct MinecraftAuthStore {
|
||||||
|
pub users: HashMap<Uuid, Credentials>,
|
||||||
|
pub token: Option<SaveDeviceToken>,
|
||||||
|
pub default_user: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinecraftAuthStore {
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn init(
|
||||||
|
dirs: &DirectoryInfo,
|
||||||
|
io_semaphore: &IoSemaphore,
|
||||||
|
) -> crate::Result<Self> {
|
||||||
|
let auth_path = dirs.caches_meta_dir().await.join(AUTH_JSON);
|
||||||
|
let store = read_json(&auth_path, io_semaphore).await.ok();
|
||||||
|
|
||||||
|
if let Some(store) = store {
|
||||||
|
Ok(store)
|
||||||
|
} else {
|
||||||
|
Ok(Self {
|
||||||
|
users: HashMap::new(),
|
||||||
|
token: None,
|
||||||
|
default_user: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn save(&self) -> crate::Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let auth_path =
|
||||||
|
state.directories.caches_meta_dir().await.join(AUTH_JSON);
|
||||||
|
|
||||||
|
write(&auth_path, &serde_json::to_vec(&self)?, &state.io_semaphore)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
async fn refresh_and_get_device_token(
|
||||||
|
&mut self,
|
||||||
|
) -> crate::Result<(DeviceTokenKey, DeviceToken)> {
|
||||||
|
macro_rules! generate_key {
|
||||||
|
($self:ident, $generate_key:expr, $device_token:expr, $SaveDeviceToken:path) => {{
|
||||||
|
let key = generate_key()?;
|
||||||
|
let token = device_token(&key).await?;
|
||||||
|
|
||||||
|
self.token = Some(SaveDeviceToken {
|
||||||
|
id: key.id.clone(),
|
||||||
|
private_key: key
|
||||||
|
.key
|
||||||
|
.to_pkcs8_pem(LineEnding::default())
|
||||||
|
.map_err(|err| {
|
||||||
|
MinecraftAuthenticationError::PEMSerialize(err)
|
||||||
|
})?
|
||||||
|
.to_string(),
|
||||||
|
x: key.x.clone(),
|
||||||
|
y: key.y.clone(),
|
||||||
|
token: token.clone(),
|
||||||
|
});
|
||||||
|
self.save().await?;
|
||||||
|
|
||||||
|
(key, token)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
let (key, token) = if let Some(ref token) = self.token {
|
||||||
|
if token.token.not_after > Utc::now() {
|
||||||
|
if let Ok(private_key) =
|
||||||
|
SigningKey::from_pkcs8_pem(&token.private_key)
|
||||||
|
{
|
||||||
|
(
|
||||||
|
DeviceTokenKey {
|
||||||
|
id: token.id.clone(),
|
||||||
|
key: private_key,
|
||||||
|
x: token.x.clone(),
|
||||||
|
y: token.y.clone(),
|
||||||
|
},
|
||||||
|
token.token.clone(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
generate_key!(
|
||||||
|
self,
|
||||||
|
generate_key,
|
||||||
|
device_token,
|
||||||
|
SaveDeviceToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generate_key!(self, generate_key, device_token, SaveDeviceToken)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generate_key!(self, generate_key, device_token, SaveDeviceToken)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((key, token))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn login_begin(&mut self) -> crate::Result<MinecraftLoginFlow> {
|
||||||
|
let (key, token) = self.refresh_and_get_device_token().await?;
|
||||||
|
|
||||||
|
let challenge = generate_oauth_challenge();
|
||||||
|
let (session_id, redirect_uri) =
|
||||||
|
sisu_authenticate(&token.token, &challenge, &key).await?;
|
||||||
|
|
||||||
|
Ok(MinecraftLoginFlow {
|
||||||
|
challenge,
|
||||||
|
session_id,
|
||||||
|
redirect_uri: redirect_uri.msa_oauth_redirect,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn login_finish(
|
||||||
|
&mut self,
|
||||||
|
code: &str,
|
||||||
|
flow: MinecraftLoginFlow,
|
||||||
|
) -> crate::Result<Credentials> {
|
||||||
|
let (key, token) = self.refresh_and_get_device_token().await?;
|
||||||
|
|
||||||
|
let oauth_token = oauth_token(code, &flow.challenge).await?;
|
||||||
|
let sisu_authorize = sisu_authorize(
|
||||||
|
Some(&flow.session_id),
|
||||||
|
&oauth_token.access_token,
|
||||||
|
&token.token,
|
||||||
|
&key,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let xbox_token =
|
||||||
|
xsts_authorize(sisu_authorize, &token.token, &key).await?;
|
||||||
|
let minecraft_token = minecraft_token(xbox_token).await?;
|
||||||
|
|
||||||
|
minecraft_entitlements(&minecraft_token.access_token).await?;
|
||||||
|
|
||||||
|
let profile = minecraft_profile(&minecraft_token.access_token).await?;
|
||||||
|
|
||||||
|
let profile_id = profile.id.unwrap_or_default();
|
||||||
|
|
||||||
|
let credentials = Credentials {
|
||||||
|
id: profile_id,
|
||||||
|
username: profile.name,
|
||||||
|
access_token: minecraft_token.access_token,
|
||||||
|
refresh_token: oauth_token.refresh_token,
|
||||||
|
expires: Utc::now()
|
||||||
|
+ Duration::seconds(oauth_token.expires_in as i64),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.users.insert(profile_id, credentials.clone());
|
||||||
|
|
||||||
|
if self.default_user.is_none() {
|
||||||
|
self.default_user = Some(profile_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save().await?;
|
||||||
|
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn get_default_credential(
|
||||||
|
&mut self,
|
||||||
|
) -> crate::Result<Option<Credentials>> {
|
||||||
|
let credentials = if let Some(default_user) = self.default_user {
|
||||||
|
if let Some(creds) = self.users.get(&default_user) {
|
||||||
|
Some(creds)
|
||||||
|
} else {
|
||||||
|
self.users.values().next()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.users.values().next()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(creds) = credentials {
|
||||||
|
if self.default_user != Some(creds.id) {
|
||||||
|
self.default_user = Some(creds.id);
|
||||||
|
self.save().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if creds.expires < Utc::now() {
|
||||||
|
let cred_id = creds.id;
|
||||||
|
let profile_name = creds.username.clone();
|
||||||
|
|
||||||
|
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
|
||||||
|
let (key, token) = self.refresh_and_get_device_token().await?;
|
||||||
|
|
||||||
|
let sisu_authorize = sisu_authorize(
|
||||||
|
None,
|
||||||
|
&oauth_token.access_token,
|
||||||
|
&token.token,
|
||||||
|
&key,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let xbox_token =
|
||||||
|
xsts_authorize(sisu_authorize, &token.token, &key).await?;
|
||||||
|
|
||||||
|
let minecraft_token = minecraft_token(xbox_token).await?;
|
||||||
|
|
||||||
|
let val = Credentials {
|
||||||
|
id: cred_id,
|
||||||
|
username: profile_name,
|
||||||
|
access_token: minecraft_token.access_token,
|
||||||
|
refresh_token: oauth_token.refresh_token,
|
||||||
|
expires: Utc::now()
|
||||||
|
+ Duration::seconds(oauth_token.expires_in as i64),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.users.insert(val.id, val.clone());
|
||||||
|
self.save().await?;
|
||||||
|
|
||||||
|
Ok(Some(val))
|
||||||
|
} else {
|
||||||
|
Ok(Some(creds.clone()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub async fn remove(
|
||||||
|
&mut self,
|
||||||
|
id: Uuid,
|
||||||
|
) -> crate::Result<Option<Credentials>> {
|
||||||
|
let val = self.users.remove(&id);
|
||||||
|
self.save().await?;
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Credentials {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
pub expires: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
||||||
|
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||||
|
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||||
|
|
||||||
|
// flow steps
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
pub struct DeviceToken {
|
||||||
|
pub issue_instant: DateTime<Utc>,
|
||||||
|
pub not_after: DateTime<Utc>,
|
||||||
|
pub token: String,
|
||||||
|
pub display_claims: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(key))]
|
||||||
|
pub async fn device_token(
|
||||||
|
key: &DeviceTokenKey,
|
||||||
|
) -> Result<DeviceToken, MinecraftAuthenticationError> {
|
||||||
|
Ok(send_signed_request(
|
||||||
|
None,
|
||||||
|
"https://device.auth.xboxlive.com/device/authenticate",
|
||||||
|
"/device/authenticate",
|
||||||
|
json!({
|
||||||
|
"Properties": {
|
||||||
|
"AuthMethod": "ProofOfPossession",
|
||||||
|
"Id": format!("{{{}}}", key.id),
|
||||||
|
"DeviceType": "Win32",
|
||||||
|
"Version": "10.16.0",
|
||||||
|
"ProofKey": {
|
||||||
|
"kty": "EC",
|
||||||
|
"x": key.x,
|
||||||
|
"y": key.y,
|
||||||
|
"crv": "P-256",
|
||||||
|
"alg": "ES256",
|
||||||
|
"use": "sig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RelyingParty": "http://auth.xboxlive.com",
|
||||||
|
"TokenType": "JWT"
|
||||||
|
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
MinecraftAuthStep::GetDeviceToken,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
struct RedirectUri {
|
||||||
|
pub msa_oauth_redirect: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(key))]
|
||||||
|
async fn sisu_authenticate(
|
||||||
|
token: &str,
|
||||||
|
challenge: &str,
|
||||||
|
key: &DeviceTokenKey,
|
||||||
|
) -> Result<(String, RedirectUri), MinecraftAuthenticationError> {
|
||||||
|
let (headers, res) = send_signed_request(
|
||||||
|
None,
|
||||||
|
"https://sisu.xboxlive.com/authenticate",
|
||||||
|
"/authenticate",
|
||||||
|
json!({
|
||||||
|
"AppId": MICROSOFT_CLIENT_ID,
|
||||||
|
"DeviceToken": token,
|
||||||
|
"Offers": [
|
||||||
|
REQUESTED_SCOPES
|
||||||
|
],
|
||||||
|
"Query": {
|
||||||
|
"code_challenge": challenge,
|
||||||
|
"code_challenge_method": "plain",
|
||||||
|
"state": "",
|
||||||
|
"prompt": "select_account"
|
||||||
|
},
|
||||||
|
"RedirectUri": REDIRECT_URL,
|
||||||
|
"Sandbox": "RETAIL",
|
||||||
|
"TokenType": "code",
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
MinecraftAuthStep::SisuAuthenicate,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let session_id = headers
|
||||||
|
.get("X-SessionId")
|
||||||
|
.and_then(|x| x.to_str().ok())
|
||||||
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok((session_id, res))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OAuthToken {
|
||||||
|
// pub token_type: String,
|
||||||
|
pub expires_in: u64,
|
||||||
|
// pub scope: String,
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
// pub user_id: String,
|
||||||
|
// pub foci: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn oauth_token(
|
||||||
|
code: &str,
|
||||||
|
challenge: &str,
|
||||||
|
) -> Result<OAuthToken, MinecraftAuthenticationError> {
|
||||||
|
let mut query = HashMap::new();
|
||||||
|
query.insert("client_id", "00000000402b5328");
|
||||||
|
query.insert("code", code);
|
||||||
|
query.insert("code_verifier", challenge);
|
||||||
|
query.insert("grant_type", "authorization_code");
|
||||||
|
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
||||||
|
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
||||||
|
|
||||||
|
let res = auth_retry(|| {
|
||||||
|
REQWEST_CLIENT
|
||||||
|
.post("https://login.live.com/oauth20_token.srf")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.form(&query)
|
||||||
|
.send()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::GetOAuthToken,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let text = res.text().await.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::GetOAuthToken,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
serde_json::from_str(&text).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::DeserializeResponse {
|
||||||
|
source,
|
||||||
|
raw: text,
|
||||||
|
step: MinecraftAuthStep::GetOAuthToken,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn oauth_refresh(
|
||||||
|
refresh_token: &str,
|
||||||
|
) -> Result<OAuthToken, MinecraftAuthenticationError> {
|
||||||
|
let mut query = HashMap::new();
|
||||||
|
query.insert("client_id", "00000000402b5328");
|
||||||
|
query.insert("refresh_token", refresh_token);
|
||||||
|
query.insert("grant_type", "refresh_token");
|
||||||
|
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
||||||
|
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
||||||
|
|
||||||
|
let res = auth_retry(|| {
|
||||||
|
REQWEST_CLIENT
|
||||||
|
.post("https://login.live.com/oauth20_token.srf")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.form(&query)
|
||||||
|
.send()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let text = res.text().await.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
serde_json::from_str(&text).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::DeserializeResponse {
|
||||||
|
source,
|
||||||
|
raw: text,
|
||||||
|
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
struct SisuAuthorize {
|
||||||
|
// pub authorization_token: DeviceToken,
|
||||||
|
// pub device_token: String,
|
||||||
|
// pub sandbox: String,
|
||||||
|
pub title_token: DeviceToken,
|
||||||
|
pub user_token: DeviceToken,
|
||||||
|
// pub web_page: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(key))]
|
||||||
|
async fn sisu_authorize(
|
||||||
|
session_id: Option<&str>,
|
||||||
|
access_token: &str,
|
||||||
|
device_token: &str,
|
||||||
|
key: &DeviceTokenKey,
|
||||||
|
) -> Result<SisuAuthorize, MinecraftAuthenticationError> {
|
||||||
|
Ok(send_signed_request(
|
||||||
|
None,
|
||||||
|
"https://sisu.xboxlive.com/authorize",
|
||||||
|
"/authorize",
|
||||||
|
json!({
|
||||||
|
"AccessToken": format!("t={access_token}"),
|
||||||
|
"AppId": "00000000402b5328",
|
||||||
|
"DeviceToken": device_token,
|
||||||
|
"ProofKey": {
|
||||||
|
"kty": "EC",
|
||||||
|
"x": key.x,
|
||||||
|
"y": key.y,
|
||||||
|
"crv": "P-256",
|
||||||
|
"alg": "ES256",
|
||||||
|
"use": "sig"
|
||||||
|
},
|
||||||
|
"Sandbox": "RETAIL",
|
||||||
|
"SessionId": session_id,
|
||||||
|
"SiteName": "user.auth.xboxlive.com",
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
MinecraftAuthStep::SisuAuthorize,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(key))]
|
||||||
|
async fn xsts_authorize(
|
||||||
|
authorize: SisuAuthorize,
|
||||||
|
device_token: &str,
|
||||||
|
key: &DeviceTokenKey,
|
||||||
|
) -> Result<DeviceToken, MinecraftAuthenticationError> {
|
||||||
|
Ok(send_signed_request(
|
||||||
|
None,
|
||||||
|
"https://xsts.auth.xboxlive.com/xsts/authorize",
|
||||||
|
"/xsts/authorize",
|
||||||
|
json!({
|
||||||
|
"RelyingParty": "rp://api.minecraftservices.com/",
|
||||||
|
"TokenType": "JWT",
|
||||||
|
"Properties": {
|
||||||
|
"SandboxId": "RETAIL",
|
||||||
|
"UserTokens": [authorize.user_token.token],
|
||||||
|
"DeviceToken": device_token,
|
||||||
|
"TitleToken": authorize.title_token.token,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
MinecraftAuthStep::XstsAuthorize,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MinecraftToken {
|
||||||
|
// pub username: String,
|
||||||
|
pub access_token: String,
|
||||||
|
// pub token_type: String,
|
||||||
|
// pub expires_in: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn minecraft_token(
|
||||||
|
token: DeviceToken,
|
||||||
|
) -> Result<MinecraftToken, MinecraftAuthenticationError> {
|
||||||
|
let uhs = token
|
||||||
|
.display_claims
|
||||||
|
.get("xui")
|
||||||
|
.and_then(|x| x.get(0))
|
||||||
|
.and_then(|x| x.get("uhs"))
|
||||||
|
.and_then(|x| x.as_str().map(String::from))
|
||||||
|
.ok_or_else(|| MinecraftAuthenticationError::NoUserHash)?;
|
||||||
|
|
||||||
|
let token = token.token;
|
||||||
|
|
||||||
|
let res = auth_retry(|| {
|
||||||
|
REQWEST_CLIENT
|
||||||
|
.post("https://api.minecraftservices.com/launcher/login")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.json(&json!({
|
||||||
|
"platform": "PC_LAUNCHER",
|
||||||
|
"xtoken": format!("XBL3.0 x={uhs};{token}"),
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::MinecraftToken,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let text = res.text().await.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::MinecraftToken,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
serde_json::from_str(&text).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::DeserializeResponse {
|
||||||
|
source,
|
||||||
|
raw: text,
|
||||||
|
step: MinecraftAuthStep::MinecraftToken,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MinecraftProfile {
|
||||||
|
pub id: Option<Uuid>,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn minecraft_profile(
|
||||||
|
token: &str,
|
||||||
|
) -> Result<MinecraftProfile, MinecraftAuthenticationError> {
|
||||||
|
let res = auth_retry(|| {
|
||||||
|
REQWEST_CLIENT
|
||||||
|
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.bearer_auth(token)
|
||||||
|
.send()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::MinecraftProfile,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let text = res.text().await.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::MinecraftProfile,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
serde_json::from_str(&text).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::DeserializeResponse {
|
||||||
|
source,
|
||||||
|
raw: text,
|
||||||
|
step: MinecraftAuthStep::MinecraftProfile,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct MinecraftEntitlements {}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn minecraft_entitlements(
|
||||||
|
token: &str,
|
||||||
|
) -> Result<MinecraftEntitlements, MinecraftAuthenticationError> {
|
||||||
|
let res = auth_retry(|| {
|
||||||
|
REQWEST_CLIENT
|
||||||
|
.get(format!("https://api.minecraftservices.com/entitlements/license?requestId={}", Uuid::new_v4()))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.bearer_auth(token)
|
||||||
|
.send()
|
||||||
|
})
|
||||||
|
.await.map_err(|source| MinecraftAuthenticationError::Request { source, step: MinecraftAuthStep::MinecraftEntitlements })?;
|
||||||
|
|
||||||
|
let text = res.text().await.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::Request {
|
||||||
|
source,
|
||||||
|
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
serde_json::from_str(&text).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::DeserializeResponse {
|
||||||
|
source,
|
||||||
|
raw: text,
|
||||||
|
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth utils
|
||||||
|
#[tracing::instrument(skip(reqwest_request))]
|
||||||
|
async fn auth_retry<F>(
|
||||||
|
reqwest_request: impl Fn() -> F,
|
||||||
|
) -> Result<reqwest::Response, reqwest::Error>
|
||||||
|
where
|
||||||
|
F: Future<Output = Result<Response, reqwest::Error>>,
|
||||||
|
{
|
||||||
|
const RETRY_COUNT: usize = 9; // Does command 9 times
|
||||||
|
const RETRY_WAIT: std::time::Duration =
|
||||||
|
std::time::Duration::from_millis(250);
|
||||||
|
|
||||||
|
let mut resp = reqwest_request().await?;
|
||||||
|
for i in 0..RETRY_COUNT {
|
||||||
|
if resp.status().is_success() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tracing::debug!(
|
||||||
|
"Request failed with status code {}, retrying...",
|
||||||
|
resp.status()
|
||||||
|
);
|
||||||
|
if i < RETRY_COUNT - 1 {
|
||||||
|
tokio::time::sleep(RETRY_WAIT).await;
|
||||||
|
}
|
||||||
|
resp = reqwest_request().await?;
|
||||||
|
}
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DeviceTokenKey {
|
||||||
|
pub id: String,
|
||||||
|
pub key: SigningKey,
|
||||||
|
pub x: String,
|
||||||
|
pub y: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
fn generate_key() -> Result<DeviceTokenKey, MinecraftAuthenticationError> {
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let signing_key = SigningKey::random(&mut OsRng);
|
||||||
|
let public_key = VerifyingKey::from(&signing_key);
|
||||||
|
|
||||||
|
let encoded_point = public_key.to_encoded_point(false);
|
||||||
|
|
||||||
|
Ok(DeviceTokenKey {
|
||||||
|
id,
|
||||||
|
key: signing_key,
|
||||||
|
x: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.x().unwrap()),
|
||||||
|
y: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.y().unwrap()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(key))]
|
||||||
|
async fn send_signed_request<T: DeserializeOwned>(
|
||||||
|
authorization: Option<&str>,
|
||||||
|
url: &str,
|
||||||
|
url_path: &str,
|
||||||
|
raw_body: serde_json::Value,
|
||||||
|
key: &DeviceTokenKey,
|
||||||
|
step: MinecraftAuthStep,
|
||||||
|
) -> Result<(HeaderMap, T), MinecraftAuthenticationError> {
|
||||||
|
let auth = authorization.map_or(Vec::new(), |v| v.as_bytes().to_vec());
|
||||||
|
|
||||||
|
let body = serde_json::to_vec(&raw_body).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::SerializeBody { source, step }
|
||||||
|
})?;
|
||||||
|
let time: u128 =
|
||||||
|
{ ((Utc::now().timestamp() as u128) + 11644473600) * 10000000 };
|
||||||
|
|
||||||
|
use byteorder::WriteBytesExt;
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
buffer.write_u32::<BigEndian>(1).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
buffer.write_u8(0).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
buffer
|
||||||
|
.write_u64::<BigEndian>(time as u64)
|
||||||
|
.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest {
|
||||||
|
source,
|
||||||
|
step,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
buffer.write_u8(0).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
buffer.extend_from_slice("POST".as_bytes());
|
||||||
|
buffer.write_u8(0).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
buffer.extend_from_slice(url_path.as_bytes());
|
||||||
|
buffer.write_u8(0).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
buffer.extend_from_slice(&auth);
|
||||||
|
buffer.write_u8(0).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
buffer.extend_from_slice(&body);
|
||||||
|
buffer.write_u8(0).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ecdsa_sig: Signature = key.key.sign(&buffer);
|
||||||
|
|
||||||
|
let mut sig_buffer = Vec::new();
|
||||||
|
sig_buffer.write_i32::<BigEndian>(1).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||||
|
})?;
|
||||||
|
sig_buffer
|
||||||
|
.write_u64::<BigEndian>(time as u64)
|
||||||
|
.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::ConstructingSignedRequest {
|
||||||
|
source,
|
||||||
|
step,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
sig_buffer.extend_from_slice(&ecdsa_sig.r().to_bytes());
|
||||||
|
sig_buffer.extend_from_slice(&ecdsa_sig.s().to_bytes());
|
||||||
|
|
||||||
|
let signature = BASE64_STANDARD.encode(&sig_buffer);
|
||||||
|
|
||||||
|
let res = auth_retry(|| {
|
||||||
|
let mut request = REQWEST_CLIENT
|
||||||
|
.post(url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("x-xbl-contract-version", "1")
|
||||||
|
.header("signature", &signature);
|
||||||
|
|
||||||
|
if let Some(auth) = authorization {
|
||||||
|
request = request.header("Authorization", auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.body(body.clone()).send()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|source| MinecraftAuthenticationError::Request { source, step })?;
|
||||||
|
|
||||||
|
let headers = res.headers().clone();
|
||||||
|
let res = res.text().await.map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::Request { source, step }
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let body = serde_json::from_str(&res).map_err(|source| {
|
||||||
|
MinecraftAuthenticationError::DeserializeResponse {
|
||||||
|
source,
|
||||||
|
raw: res,
|
||||||
|
step,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok((headers, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
fn generate_oauth_challenge() -> String {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
let bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
|
||||||
|
bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ use std::path::PathBuf;
|
|||||||
use crate::event::LoadingBarType;
|
use crate::event::LoadingBarType;
|
||||||
use crate::loading_join;
|
use crate::loading_join;
|
||||||
|
|
||||||
use crate::state::users::Users;
|
|
||||||
use crate::util::fetch::{self, FetchSemaphore, IoSemaphore};
|
use crate::util::fetch::{self, FetchSemaphore, IoSemaphore};
|
||||||
use notify::RecommendedWatcher;
|
use notify::RecommendedWatcher;
|
||||||
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
|
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
|
||||||
@ -32,14 +31,9 @@ pub use self::settings::*;
|
|||||||
mod projects;
|
mod projects;
|
||||||
pub use self::projects::*;
|
pub use self::projects::*;
|
||||||
|
|
||||||
mod users;
|
|
||||||
|
|
||||||
mod children;
|
mod children;
|
||||||
pub use self::children::*;
|
pub use self::children::*;
|
||||||
|
|
||||||
mod auth_task;
|
|
||||||
pub use self::auth_task::*;
|
|
||||||
|
|
||||||
mod tags;
|
mod tags;
|
||||||
pub use self::tags::*;
|
pub use self::tags::*;
|
||||||
|
|
||||||
@ -52,6 +46,9 @@ pub use self::safe_processes::*;
|
|||||||
mod discord;
|
mod discord;
|
||||||
pub use self::discord::*;
|
pub use self::discord::*;
|
||||||
|
|
||||||
|
mod minecraft_auth;
|
||||||
|
pub use self::minecraft_auth::*;
|
||||||
|
|
||||||
mod mr_auth;
|
mod mr_auth;
|
||||||
pub use self::mr_auth::*;
|
pub use self::mr_auth::*;
|
||||||
|
|
||||||
@ -87,9 +84,7 @@ pub struct State {
|
|||||||
/// Launcher processes that should be safely exited on shutdown
|
/// Launcher processes that should be safely exited on shutdown
|
||||||
pub(crate) safety_processes: RwLock<SafeProcesses>,
|
pub(crate) safety_processes: RwLock<SafeProcesses>,
|
||||||
/// Launcher user account info
|
/// Launcher user account info
|
||||||
pub(crate) users: RwLock<Users>,
|
pub(crate) users: RwLock<MinecraftAuthStore>,
|
||||||
/// Authentication flow
|
|
||||||
pub auth_flow: RwLock<AuthTask>,
|
|
||||||
/// Modrinth Credentials Store
|
/// Modrinth Credentials Store
|
||||||
pub credentials: RwLock<CredentialsStore>,
|
pub credentials: RwLock<CredentialsStore>,
|
||||||
/// Modrinth auth flow
|
/// Modrinth auth flow
|
||||||
@ -172,7 +167,7 @@ impl State {
|
|||||||
&fetch_semaphore,
|
&fetch_semaphore,
|
||||||
&CredentialsStore(None),
|
&CredentialsStore(None),
|
||||||
);
|
);
|
||||||
let users_fut = Users::init(&directories, &io_semaphore);
|
let users_fut = MinecraftAuthStore::init(&directories, &io_semaphore);
|
||||||
let creds_fut = CredentialsStore::init(&directories, &io_semaphore);
|
let creds_fut = CredentialsStore::init(&directories, &io_semaphore);
|
||||||
// Launcher data
|
// Launcher data
|
||||||
let (metadata, profiles, tags, users, creds) = loading_join! {
|
let (metadata, profiles, tags, users, creds) = loading_join! {
|
||||||
@ -184,7 +179,6 @@ impl State {
|
|||||||
creds_fut,
|
creds_fut,
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let auth_flow = AuthTask::new();
|
|
||||||
let safety_processes = SafeProcesses::new();
|
let safety_processes = SafeProcesses::new();
|
||||||
|
|
||||||
let discord_rpc = DiscordGuard::init(is_offline).await?;
|
let discord_rpc = DiscordGuard::init(is_offline).await?;
|
||||||
@ -217,7 +211,6 @@ impl State {
|
|||||||
profiles: RwLock::new(profiles),
|
profiles: RwLock::new(profiles),
|
||||||
users: RwLock::new(users),
|
users: RwLock::new(users),
|
||||||
children: RwLock::new(children),
|
children: RwLock::new(children),
|
||||||
auth_flow: RwLock::new(auth_flow),
|
|
||||||
credentials: RwLock::new(creds),
|
credentials: RwLock::new(creds),
|
||||||
tags: RwLock::new(tags),
|
tags: RwLock::new(tags),
|
||||||
discord_rpc,
|
discord_rpc,
|
||||||
@ -253,9 +246,8 @@ impl State {
|
|||||||
let res4 = Profiles::update_projects();
|
let res4 = Profiles::update_projects();
|
||||||
let res5 = Settings::update_java();
|
let res5 = Settings::update_java();
|
||||||
let res6 = CredentialsStore::update_creds();
|
let res6 = CredentialsStore::update_creds();
|
||||||
let res7 = Settings::update_default_user();
|
|
||||||
|
|
||||||
let _ = join!(res1, res2, res3, res4, res5, res6, res7);
|
let _ = join!(res1, res2, res3, res4, res5, res6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -24,7 +24,6 @@ pub struct Settings {
|
|||||||
pub custom_java_args: Vec<String>,
|
pub custom_java_args: Vec<String>,
|
||||||
pub custom_env_args: Vec<(String, String)>,
|
pub custom_env_args: Vec<(String, String)>,
|
||||||
pub java_globals: JavaGlobals,
|
pub java_globals: JavaGlobals,
|
||||||
pub default_user: Option<uuid::Uuid>,
|
|
||||||
pub hooks: Hooks,
|
pub hooks: Hooks,
|
||||||
pub max_concurrent_downloads: usize,
|
pub max_concurrent_downloads: usize,
|
||||||
pub max_concurrent_writes: usize,
|
pub max_concurrent_writes: usize,
|
||||||
@ -93,7 +92,6 @@ impl Settings {
|
|||||||
custom_java_args: Vec::new(),
|
custom_java_args: Vec::new(),
|
||||||
custom_env_args: Vec::new(),
|
custom_env_args: Vec::new(),
|
||||||
java_globals: JavaGlobals::new(),
|
java_globals: JavaGlobals::new(),
|
||||||
default_user: None,
|
|
||||||
hooks: Hooks::default(),
|
hooks: Hooks::default(),
|
||||||
max_concurrent_downloads: 10,
|
max_concurrent_downloads: 10,
|
||||||
max_concurrent_writes: 10,
|
max_concurrent_writes: 10,
|
||||||
@ -152,32 +150,6 @@ impl Settings {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
#[theseus_macros::debug_pin]
|
|
||||||
pub async fn update_default_user() {
|
|
||||||
let res = async {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let settings_read = state.settings.read().await;
|
|
||||||
|
|
||||||
if settings_read.default_user.is_none() {
|
|
||||||
drop(settings_read);
|
|
||||||
let users = state.users.read().await;
|
|
||||||
let user = users.0.iter().next().map(|(id, _)| *id);
|
|
||||||
state.settings.write().await.default_user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), crate::Error>(())
|
|
||||||
}
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(err) => {
|
|
||||||
tracing::warn!("Unable to update default user: {err}")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||||
fs::write(to, serde_json::to_vec(self)?)
|
fs::write(to, serde_json::to_vec(self)?)
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
//! User login info
|
|
||||||
use crate::auth::Credentials;
|
|
||||||
use crate::data::DirectoryInfo;
|
|
||||||
use crate::util::fetch::{read_json, write, IoSemaphore};
|
|
||||||
use crate::State;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
const USERS_JSON: &str = "users.json";
|
|
||||||
|
|
||||||
/// The set of users stored in the launcher
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct Users(pub(crate) HashMap<Uuid, Credentials>);
|
|
||||||
|
|
||||||
impl Users {
|
|
||||||
pub async fn init(
|
|
||||||
dirs: &DirectoryInfo,
|
|
||||||
io_semaphore: &IoSemaphore,
|
|
||||||
) -> crate::Result<Self> {
|
|
||||||
let users_path = dirs.caches_meta_dir().await.join(USERS_JSON);
|
|
||||||
let users = read_json(&users_path, io_semaphore).await.ok();
|
|
||||||
|
|
||||||
if let Some(users) = users {
|
|
||||||
Ok(Self(users))
|
|
||||||
} else {
|
|
||||||
Ok(Self(HashMap::new()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn save(&self) -> crate::Result<()> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let users_path =
|
|
||||||
state.directories.caches_meta_dir().await.join(USERS_JSON);
|
|
||||||
write(
|
|
||||||
&users_path,
|
|
||||||
&serde_json::to_vec(&self.0)?,
|
|
||||||
&state.io_semaphore,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub async fn insert(
|
|
||||||
&mut self,
|
|
||||||
credentials: &Credentials,
|
|
||||||
) -> crate::Result<&Self> {
|
|
||||||
self.0.insert(credentials.id, credentials.clone());
|
|
||||||
self.save().await?;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
|
||||||
pub fn contains(&self, id: Uuid) -> bool {
|
|
||||||
self.0.contains_key(&id)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
|
||||||
pub fn get(&self, id: Uuid) -> Option<Credentials> {
|
|
||||||
self.0.get(&id).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
|
||||||
pub async fn remove(&mut self, id: Uuid) -> crate::Result<&Self> {
|
|
||||||
self.0.remove(&id);
|
|
||||||
self.save().await?;
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "theseus_cli"
|
|
||||||
version = "0.6.3"
|
|
||||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
theseus = { path = "../theseus", features = ["cli"] }
|
|
||||||
daedalus = {version = "0.1.15", features = ["bincode"]}
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
tokio-stream = { version = "0.1", features = ["fs"] }
|
|
||||||
futures = "0.3"
|
|
||||||
argh = "0.1"
|
|
||||||
paris = { version = "1.5", features = ["macros", "no_logger"] }
|
|
||||||
dialoguer = "0.11.0"
|
|
||||||
tabled = "0.15.0"
|
|
||||||
dirs = "5.0.1"
|
|
||||||
uuid = {version = "1.1", features = ["v4", "serde"]}
|
|
||||||
url = "2.2"
|
|
||||||
|
|
||||||
color-eyre = "0.6"
|
|
||||||
eyre = "0.6"
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-error = "0.2"
|
|
||||||
tracing-futures = "0.2"
|
|
||||||
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
|
|
||||||
dunce = "1.0.3"
|
|
||||||
|
|
||||||
webbrowser = "0.8.13"
|
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
|
||||||
winreg = "0.52.0"
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
use eyre::Result;
|
|
||||||
use futures::TryFutureExt;
|
|
||||||
use paris::*;
|
|
||||||
use tracing_error::ErrorLayer;
|
|
||||||
use tracing_futures::WithSubscriber;
|
|
||||||
use tracing_subscriber::prelude::*;
|
|
||||||
use tracing_subscriber::{fmt, EnvFilter};
|
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
mod subcommands;
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// The official Modrinth CLI
|
|
||||||
pub struct Args {
|
|
||||||
#[argh(subcommand)]
|
|
||||||
pub subcommand: subcommands::Subcommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let args = argh::from_env::<Args>();
|
|
||||||
|
|
||||||
color_eyre::install()?;
|
|
||||||
let filter = EnvFilter::try_from_default_env()
|
|
||||||
.or_else(|_| EnvFilter::try_new("info"))?;
|
|
||||||
|
|
||||||
let format = fmt::layer()
|
|
||||||
.without_time()
|
|
||||||
.with_writer(std::io::stderr)
|
|
||||||
.with_target(false)
|
|
||||||
.compact();
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(format)
|
|
||||||
.with(filter)
|
|
||||||
.with(ErrorLayer::default())
|
|
||||||
.init();
|
|
||||||
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?
|
|
||||||
.block_on(
|
|
||||||
async move {
|
|
||||||
args.dispatch()
|
|
||||||
.inspect_err(|_| error!("An error has occurred!\n"))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
.with_current_subscriber(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
use eyre::Result;
|
|
||||||
|
|
||||||
mod profile;
|
|
||||||
mod user;
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
#[argh(subcommand)]
|
|
||||||
pub enum Subcommand {
|
|
||||||
Profile(profile::ProfileCommand),
|
|
||||||
User(user::UserCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl crate::Args {
|
|
||||||
pub async fn dispatch(&self) -> Result<()> {
|
|
||||||
dispatch!(self.subcommand, (self) => {
|
|
||||||
Subcommand::Profile,
|
|
||||||
Subcommand::User
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,372 +0,0 @@
|
|||||||
//! Profile management subcommand
|
|
||||||
use crate::util::table_path_display;
|
|
||||||
use crate::util::{confirm_async, prompt_async, select_async, table};
|
|
||||||
use daedalus::modded::LoaderVersion;
|
|
||||||
use dunce::canonicalize;
|
|
||||||
use eyre::{ensure, Result};
|
|
||||||
use futures::prelude::*;
|
|
||||||
use paris::*;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use tabled::settings::object::Columns;
|
|
||||||
use tabled::settings::{Modify, Width};
|
|
||||||
use tabled::Tabled;
|
|
||||||
use theseus::prelude::*;
|
|
||||||
use theseus::profile::create::profile_create;
|
|
||||||
use tokio::fs;
|
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
#[argh(subcommand, name = "profile")]
|
|
||||||
/// manage Minecraft instances
|
|
||||||
pub struct ProfileCommand {
|
|
||||||
#[argh(subcommand)]
|
|
||||||
action: ProfileSubcommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
#[argh(subcommand)]
|
|
||||||
pub enum ProfileSubcommand {
|
|
||||||
Init(ProfileInit),
|
|
||||||
List(ProfileList),
|
|
||||||
Remove(ProfileRemove),
|
|
||||||
Run(ProfileRun),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
#[argh(subcommand, name = "init")]
|
|
||||||
/// create a new profile and manage it with Theseus
|
|
||||||
pub struct ProfileInit {
|
|
||||||
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
|
||||||
/// the path of the newly created profile
|
|
||||||
path: PathBuf,
|
|
||||||
|
|
||||||
#[argh(option)]
|
|
||||||
/// the name of the profile
|
|
||||||
name: Option<String>,
|
|
||||||
|
|
||||||
#[argh(option)]
|
|
||||||
/// the game version of the profile
|
|
||||||
game_version: Option<String>,
|
|
||||||
|
|
||||||
#[argh(option, from_str_fn(modloader_from_str))]
|
|
||||||
/// the modloader to use
|
|
||||||
modloader: Option<ModLoader>,
|
|
||||||
|
|
||||||
#[argh(option)]
|
|
||||||
/// the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
|
||||||
loader_version: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileInit {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &ProfileCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
// TODO: validate inputs from args early
|
|
||||||
let state = State::get().await?;
|
|
||||||
let metadata = state.metadata.read().await;
|
|
||||||
|
|
||||||
if self.path.exists() {
|
|
||||||
ensure!(
|
|
||||||
self.path.is_dir(),
|
|
||||||
"Attempted to create profile in something other than a folder!"
|
|
||||||
);
|
|
||||||
ensure!(
|
|
||||||
!self.path.join("profile.json").exists(),
|
|
||||||
"Profile already exists! Perhaps you want `profile add` instead?"
|
|
||||||
);
|
|
||||||
if ReadDirStream::new(fs::read_dir(&self.path).await?)
|
|
||||||
.next()
|
|
||||||
.await
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
warn!("You are trying to create a profile in a non-empty directory. If this is an instance from another launcher, please be sure to properly fill the profile.json fields!");
|
|
||||||
if !confirm_async(
|
|
||||||
String::from("Do you wish to continue"),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
eyre::bail!("Aborted!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fs::create_dir_all(&self.path).await?;
|
|
||||||
}
|
|
||||||
info!(
|
|
||||||
"Creating profile at path {}",
|
|
||||||
&canonicalize(&self.path)?.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: abstract default prompting
|
|
||||||
let name = match &self.name {
|
|
||||||
Some(name) => name.clone(),
|
|
||||||
None => {
|
|
||||||
let default = self.path.file_name().unwrap().to_string_lossy();
|
|
||||||
|
|
||||||
prompt_async(
|
|
||||||
String::from("Instance name"),
|
|
||||||
Some(default.into_owned()),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let game_version = match &self.game_version {
|
|
||||||
Some(version) => version.clone(),
|
|
||||||
None => {
|
|
||||||
let default = &metadata.minecraft.latest.release;
|
|
||||||
|
|
||||||
prompt_async(
|
|
||||||
String::from("Game version"),
|
|
||||||
Some(default.clone()),
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let loader = match &self.modloader {
|
|
||||||
Some(loader) => *loader,
|
|
||||||
None => {
|
|
||||||
let choice = select_async(
|
|
||||||
"Modloader".to_owned(),
|
|
||||||
&["vanilla", "fabric", "forge"],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
match choice {
|
|
||||||
0 => ModLoader::Vanilla,
|
|
||||||
1 => ModLoader::Fabric,
|
|
||||||
2 => ModLoader::Forge,
|
|
||||||
_ => eyre::bail!(
|
|
||||||
"Invalid modloader ID: {choice}. This is a bug in the launcher!"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let loader = if loader != ModLoader::Vanilla {
|
|
||||||
let version = match &self.loader_version {
|
|
||||||
Some(version) => String::from(version),
|
|
||||||
None => prompt_async(
|
|
||||||
String::from(
|
|
||||||
"Modloader version (latest, stable, or a version ID)",
|
|
||||||
),
|
|
||||||
Some(String::from("latest")),
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter = |it: &LoaderVersion| match version.as_str() {
|
|
||||||
"latest" => true,
|
|
||||||
"stable" => it.stable,
|
|
||||||
id => it.id == *id,
|
|
||||||
};
|
|
||||||
|
|
||||||
let loader_data = match loader {
|
|
||||||
ModLoader::Forge => &metadata.forge,
|
|
||||||
ModLoader::Fabric => &metadata.fabric,
|
|
||||||
_ => eyre::bail!("Could not get manifest for loader {loader}. This is a bug in the CLI!"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let loaders = &loader_data.game_versions
|
|
||||||
.iter()
|
|
||||||
.find(|it| it.id == game_version)
|
|
||||||
.ok_or_else(|| eyre::eyre!("Modloader {loader} unsupported for Minecraft version {game_version}"))?
|
|
||||||
.loaders;
|
|
||||||
|
|
||||||
let loader_version = loaders
|
|
||||||
.iter()
|
|
||||||
.find(|&x| filter(x))
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
eyre::eyre!("Invalid version {version} for modloader {loader}")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Some((loader_version, loader))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
profile_create(
|
|
||||||
name,
|
|
||||||
game_version,
|
|
||||||
loader.clone().map(|x| x.1).unwrap_or(ModLoader::Vanilla),
|
|
||||||
loader.map(|x| x.0.id),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
success!(
|
|
||||||
"Successfully created instance, it is now available to use with Theseus!"
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// list all managed profiles
|
|
||||||
#[argh(subcommand, name = "list")]
|
|
||||||
pub struct ProfileList {}
|
|
||||||
|
|
||||||
#[derive(Tabled)]
|
|
||||||
struct ProfileRow<'a> {
|
|
||||||
name: &'a str,
|
|
||||||
#[tabled(display_with = "table_path_display")]
|
|
||||||
path: &'a Path,
|
|
||||||
#[tabled(rename = "game version")]
|
|
||||||
game_version: &'a str,
|
|
||||||
loader: &'a ModLoader,
|
|
||||||
#[tabled(rename = "loader version")]
|
|
||||||
loader_version: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a Profile> for ProfileRow<'a> {
|
|
||||||
fn from(it: &'a Profile) -> Self {
|
|
||||||
Self {
|
|
||||||
name: &it.metadata.name,
|
|
||||||
path: Path::new(&it.metadata.name),
|
|
||||||
game_version: &it.metadata.game_version,
|
|
||||||
loader: &it.metadata.loader,
|
|
||||||
loader_version: it
|
|
||||||
.metadata
|
|
||||||
.loader_version
|
|
||||||
.as_ref()
|
|
||||||
.map_or("", |it| &it.id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a Path> for ProfileRow<'a> {
|
|
||||||
fn from(it: &'a Path) -> Self {
|
|
||||||
Self {
|
|
||||||
name: "?",
|
|
||||||
path: it,
|
|
||||||
game_version: "?",
|
|
||||||
loader: &ModLoader::Vanilla,
|
|
||||||
loader_version: "?",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileList {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &ProfileCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
let profiles = profile::list(None).await?;
|
|
||||||
let rows = profiles.values().map(ProfileRow::from);
|
|
||||||
|
|
||||||
let mut table = table(rows);
|
|
||||||
table.with(Modify::new(Columns::new(1..=1)).with(Width::wrap(40)));
|
|
||||||
|
|
||||||
println!("{table}");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// unmanage a profile
|
|
||||||
#[argh(subcommand, name = "remove")]
|
|
||||||
pub struct ProfileRemove {
|
|
||||||
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
|
||||||
/// the profile to get rid of
|
|
||||||
profile: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileRemove {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &ProfileCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
let profile =
|
|
||||||
ProfilePathId::from_fs_path(canonicalize(&self.profile)?).await?;
|
|
||||||
info!("Removing profile {} from Theseus", self.profile.display());
|
|
||||||
|
|
||||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
|
||||||
profile::remove(&profile).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
success!("Profile removed!");
|
|
||||||
} else {
|
|
||||||
error!("Aborted!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// run a profile
|
|
||||||
#[argh(subcommand, name = "run")]
|
|
||||||
pub struct ProfileRun {
|
|
||||||
#[argh(positional, default = "std::env::current_dir().unwrap()")]
|
|
||||||
/// the profile to run
|
|
||||||
profile: PathBuf,
|
|
||||||
|
|
||||||
#[argh(option)]
|
|
||||||
/// the user to authenticate with
|
|
||||||
user: Option<uuid::Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileRun {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &ProfileCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Starting profile at path {}...", self.profile.display());
|
|
||||||
let path = canonicalize(&self.profile)?;
|
|
||||||
|
|
||||||
let id = future::ready(self.user.ok_or(()))
|
|
||||||
.or_else(|_| async move {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let settings = state.settings.read().await;
|
|
||||||
|
|
||||||
settings.default_user
|
|
||||||
.ok_or(eyre::eyre!(
|
|
||||||
"Could not find any users, please add one using the `user add` command."
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let credentials = auth::refresh(id).await?;
|
|
||||||
|
|
||||||
let profile_path_id = ProfilePathId::from_fs_path(path).await?;
|
|
||||||
let proc_lock =
|
|
||||||
profile::run_credentials(&profile_path_id, &credentials).await?;
|
|
||||||
let mut proc = proc_lock.write().await;
|
|
||||||
process::wait_for(&mut proc).await?;
|
|
||||||
|
|
||||||
success!("Process exited successfully!");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfileCommand {
|
|
||||||
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
|
||||||
dispatch!(&self.action, (args, self) => {
|
|
||||||
ProfileSubcommand::Init,
|
|
||||||
ProfileSubcommand::List,
|
|
||||||
ProfileSubcommand::Remove,
|
|
||||||
ProfileSubcommand::Run
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn modloader_from_str(it: &str) -> core::result::Result<ModLoader, String> {
|
|
||||||
match it {
|
|
||||||
"vanilla" => Ok(ModLoader::Vanilla),
|
|
||||||
"forge" => Ok(ModLoader::Forge),
|
|
||||||
"fabric" => Ok(ModLoader::Fabric),
|
|
||||||
"quilt" => Ok(ModLoader::Quilt),
|
|
||||||
"neoforge" => Ok(ModLoader::NeoForge),
|
|
||||||
_ => Err(String::from("Invalid modloader: {it}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
//! User management subcommand
|
|
||||||
use crate::util::{confirm_async, table};
|
|
||||||
use eyre::Result;
|
|
||||||
use paris::*;
|
|
||||||
use tabled::Tabled;
|
|
||||||
use theseus::prelude::*;
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
#[argh(subcommand, name = "user")]
|
|
||||||
/// manage Minecraft accounts
|
|
||||||
pub struct UserCommand {
|
|
||||||
#[argh(subcommand)]
|
|
||||||
action: UserSubcommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
#[argh(subcommand)]
|
|
||||||
pub enum UserSubcommand {
|
|
||||||
Add(UserAdd),
|
|
||||||
List(UserList),
|
|
||||||
Remove(UserRemove),
|
|
||||||
SetDefault(UserDefault),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// add a new user to Theseus
|
|
||||||
#[argh(subcommand, name = "add")]
|
|
||||||
pub struct UserAdd {
|
|
||||||
#[argh(option)]
|
|
||||||
/// the browser to authenticate using
|
|
||||||
browser: Option<webbrowser::Browser>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserAdd {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &UserCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Adding new user account to Theseus");
|
|
||||||
info!("A browser window will now open, follow the login flow there.");
|
|
||||||
|
|
||||||
let login = auth::authenticate_begin_flow().await?;
|
|
||||||
let flow = tokio::spawn(auth::authenticate_await_complete_flow());
|
|
||||||
|
|
||||||
info!("Opening browser window at {}", login.verification_uri);
|
|
||||||
info!("Your code is {}", login.user_code);
|
|
||||||
|
|
||||||
match self.browser {
|
|
||||||
Some(browser) => webbrowser::open_browser(
|
|
||||||
browser,
|
|
||||||
login.verification_uri.as_str(),
|
|
||||||
),
|
|
||||||
None => webbrowser::open(login.verification_uri.as_str()),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
let credentials = flow.await??;
|
|
||||||
State::sync().await?;
|
|
||||||
success!("Logged in user {}.", credentials.username);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// list all known users
|
|
||||||
#[argh(subcommand, name = "list")]
|
|
||||||
pub struct UserList {}
|
|
||||||
|
|
||||||
#[derive(Tabled)]
|
|
||||||
struct UserRow<'a> {
|
|
||||||
username: &'a str,
|
|
||||||
id: uuid::Uuid,
|
|
||||||
default: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> UserRow<'a> {
|
|
||||||
pub fn from(
|
|
||||||
credentials: &'a Credentials,
|
|
||||||
default: Option<uuid::Uuid>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
username: &credentials.username,
|
|
||||||
id: credentials.id,
|
|
||||||
default: Some(credentials.id) == default,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserList {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &UserCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
let state = State::get().await?;
|
|
||||||
let default = state.settings.read().await.default_user;
|
|
||||||
|
|
||||||
let users = auth::users().await?;
|
|
||||||
let rows = users.iter().map(|user| UserRow::from(user, default));
|
|
||||||
|
|
||||||
let table = table(rows);
|
|
||||||
println!("{table}");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// remove a user
|
|
||||||
#[argh(subcommand, name = "remove")]
|
|
||||||
pub struct UserRemove {
|
|
||||||
/// the user to remove
|
|
||||||
#[argh(positional)]
|
|
||||||
user: uuid::Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserRemove {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &UserCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Removing user {}", self.user.as_hyphenated());
|
|
||||||
|
|
||||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
|
||||||
if !auth::has_user(self.user).await? {
|
|
||||||
warn!("Profile was not managed by Theseus!");
|
|
||||||
} else {
|
|
||||||
auth::remove_user(self.user).await?;
|
|
||||||
State::sync().await?;
|
|
||||||
success!("User removed!");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
error!("Aborted!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs, Debug)]
|
|
||||||
/// set the default user
|
|
||||||
#[argh(subcommand, name = "set-default")]
|
|
||||||
pub struct UserDefault {
|
|
||||||
/// the user to set as default
|
|
||||||
#[argh(positional)]
|
|
||||||
user: uuid::Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserDefault {
|
|
||||||
pub async fn run(
|
|
||||||
&self,
|
|
||||||
_args: &crate::Args,
|
|
||||||
_largs: &UserCommand,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("Setting user {} as default", self.user.as_hyphenated());
|
|
||||||
|
|
||||||
let state = State::get().await?;
|
|
||||||
let mut settings = state.settings.write().await;
|
|
||||||
|
|
||||||
if settings.default_user == Some(self.user) {
|
|
||||||
warn!("User is already the default!");
|
|
||||||
} else {
|
|
||||||
settings.default_user = Some(self.user);
|
|
||||||
success!("User set as default!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserCommand {
|
|
||||||
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
|
||||||
dispatch!(&self.action, (args, self) => {
|
|
||||||
UserSubcommand::Add,
|
|
||||||
UserSubcommand::List,
|
|
||||||
UserSubcommand::Remove,
|
|
||||||
UserSubcommand::SetDefault
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
use dialoguer::{Confirm, Input, Select};
|
|
||||||
use eyre::Result;
|
|
||||||
use std::{borrow::Cow, path::Path};
|
|
||||||
use tabled::settings::Style;
|
|
||||||
use tabled::{Table, Tabled};
|
|
||||||
|
|
||||||
// TODO: make primarily async to avoid copies
|
|
||||||
|
|
||||||
// Prompting helpers
|
|
||||||
pub fn prompt(prompt: &str, default: Option<String>) -> Result<String> {
|
|
||||||
let prompt = match default.as_deref() {
|
|
||||||
Some("") => Cow::Owned(format!("{prompt} (optional)")),
|
|
||||||
Some(default) => Cow::Owned(format!("{prompt} (default: {default})")),
|
|
||||||
None => Cow::Borrowed(prompt),
|
|
||||||
};
|
|
||||||
print_prompt(&prompt);
|
|
||||||
|
|
||||||
let mut input = Input::<String>::new().with_prompt("").show_default(false);
|
|
||||||
|
|
||||||
if let Some(default) = default {
|
|
||||||
input = input.default(default);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(input.interact_text()?.trim().to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn prompt_async(
|
|
||||||
text: String,
|
|
||||||
default: Option<String>,
|
|
||||||
) -> Result<String> {
|
|
||||||
tokio::task::spawn_blocking(move || prompt(&text, default)).await?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selection helpers
|
|
||||||
pub fn select(prompt: &str, choices: &[&str]) -> Result<usize> {
|
|
||||||
print_prompt(prompt);
|
|
||||||
|
|
||||||
let res = Select::new().items(choices).default(0).interact()?;
|
|
||||||
eprintln!("> {}", choices[res]);
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn select_async(
|
|
||||||
prompt: String,
|
|
||||||
choices: &'static [&'static str],
|
|
||||||
) -> Result<usize> {
|
|
||||||
tokio::task::spawn_blocking(move || select(&prompt, choices)).await?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirmation helpers
|
|
||||||
pub fn confirm(prompt: &str, default: bool) -> Result<bool> {
|
|
||||||
print_prompt(prompt);
|
|
||||||
Ok(Confirm::new().default(default).interact()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn confirm_async(prompt: String, default: bool) -> Result<bool> {
|
|
||||||
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table helpers
|
|
||||||
pub fn table<T: Tabled>(rows: impl IntoIterator<Item = T>) -> Table {
|
|
||||||
Table::new(rows).with(Style::psql()).clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn table_path_display(path: &Path) -> String {
|
|
||||||
let mut res = path.display().to_string();
|
|
||||||
|
|
||||||
if let Some(home_dir) = dirs::home_dir() {
|
|
||||||
res = res.replace(&home_dir.display().to_string(), "~");
|
|
||||||
}
|
|
||||||
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch macros
|
|
||||||
macro_rules! dispatch {
|
|
||||||
($on:expr, $args:tt => {$($option:path),+}) => {
|
|
||||||
match $on {
|
|
||||||
$($option (ref cmd) => dispatch!(@apply cmd => $args)),+
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(@apply $cmd:expr => ($($args:expr),*)) => {{
|
|
||||||
use tracing_futures::WithSubscriber;
|
|
||||||
$cmd.run($($args),*).with_current_subscriber().await
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal helpers
|
|
||||||
fn print_prompt(prompt: &str) {
|
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
paris::formatter::colorize_string(format!("<yellow>?</> {prompt}:"))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -20,7 +20,6 @@
|
|||||||
"ofetch": "^1.3.4",
|
"ofetch": "^1.3.4",
|
||||||
"omorphia": "^0.4.41",
|
"omorphia": "^0.4.41",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"qrcode.vue": "^3.4.1",
|
|
||||||
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
|
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
|
|||||||
284
theseus_gui/pnpm-lock.yaml
generated
284
theseus_gui/pnpm-lock.yaml
generated
@ -26,9 +26,6 @@ dependencies:
|
|||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.1.7
|
specifier: ^2.1.7
|
||||||
version: 2.1.7(vue@3.4.21)
|
version: 2.1.7(vue@3.4.21)
|
||||||
qrcode.vue:
|
|
||||||
specifier: ^3.4.1
|
|
||||||
version: 3.4.1(vue@3.4.21)
|
|
||||||
tauri-plugin-window-state-api:
|
tauri-plugin-window-state-api:
|
||||||
specifier: github:tauri-apps/tauri-plugin-window-state#v1
|
specifier: github:tauri-apps/tauri-plugin-window-state#v1
|
||||||
version: github.com/tauri-apps/tauri-plugin-window-state/002cf15f6a1e4969a678a4ade680cd60477a8a53
|
version: github.com/tauri-apps/tauri-plugin-window-state/002cf15f6a1e4969a678a4ade680cd60477a8a53
|
||||||
@ -87,12 +84,12 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@babel/helper-string-parser@7.24.1:
|
/@babel/helper-string-parser@7.21.5:
|
||||||
resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==}
|
resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
/@babel/helper-validator-identifier@7.22.20:
|
/@babel/helper-validator-identifier@7.19.1:
|
||||||
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
|
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
/@babel/parser@7.24.4:
|
/@babel/parser@7.24.4:
|
||||||
@ -100,14 +97,14 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.24.0
|
'@babel/types': 7.22.4
|
||||||
|
|
||||||
/@babel/types@7.24.0:
|
/@babel/types@7.22.4:
|
||||||
resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
|
resolution: {integrity: sha512-Tx9x3UBHTTsMSW85WB2kphxYQVvrZ/t1FxD88IpSgIjiUJlCm9z+xWIDwyo1vffTwSqteqyznB8ZE9vYYk16zA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-string-parser': 7.24.1
|
'@babel/helper-string-parser': 7.21.5
|
||||||
'@babel/helper-validator-identifier': 7.22.20
|
'@babel/helper-validator-identifier': 7.19.1
|
||||||
to-fast-properties: 2.0.0
|
to-fast-properties: 2.0.0
|
||||||
|
|
||||||
/@esbuild/aix-ppc64@0.20.2:
|
/@esbuild/aix-ppc64@0.20.2:
|
||||||
@ -340,7 +337,7 @@ packages:
|
|||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.24.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.1
|
ignore: 5.2.4
|
||||||
import-fresh: 3.3.0
|
import-fresh: 3.3.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
@ -354,12 +351,22 @@ packages:
|
|||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@floating-ui/core@0.3.1:
|
||||||
|
resolution: {integrity: sha512-ensKY7Ub59u16qsVIFEo2hwTCqZ/r9oZZFh51ivcLGHfUwTn8l1Xzng8RJUe91H/UP8PeqeBronAGx0qmzwk2g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@floating-ui/core@1.6.0:
|
/@floating-ui/core@1.6.0:
|
||||||
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
|
resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/utils': 0.2.1
|
'@floating-ui/utils': 0.2.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@floating-ui/dom@0.1.10:
|
||||||
|
resolution: {integrity: sha512-4kAVoogvQm2N0XE0G6APQJuCNuErjOfPW8Ux7DFxh8+AfugWflwVJ5LDlHOwrwut7z/30NUvdtHzQ3zSip4EzQ==}
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 0.3.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@floating-ui/dom@1.1.1:
|
/@floating-ui/dom@1.1.1:
|
||||||
resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==}
|
resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -415,7 +422,7 @@ packages:
|
|||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.scandir': 2.1.5
|
'@nodelib/fs.scandir': 2.1.5
|
||||||
fastq: 1.17.1
|
fastq: 1.15.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@rollup/plugin-alias@5.1.0:
|
/@rollup/plugin-alias@5.1.0:
|
||||||
@ -438,120 +445,120 @@ packages:
|
|||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@rollup/rollup-android-arm-eabi@4.14.0:
|
/@rollup/rollup-android-arm-eabi@4.14.1:
|
||||||
resolution: {integrity: sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==}
|
resolution: {integrity: sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-android-arm64@4.14.0:
|
/@rollup/rollup-android-arm64@4.14.1:
|
||||||
resolution: {integrity: sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==}
|
resolution: {integrity: sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-darwin-arm64@4.14.0:
|
/@rollup/rollup-darwin-arm64@4.14.1:
|
||||||
resolution: {integrity: sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==}
|
resolution: {integrity: sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-darwin-x64@4.14.0:
|
/@rollup/rollup-darwin-x64@4.14.1:
|
||||||
resolution: {integrity: sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==}
|
resolution: {integrity: sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-arm-gnueabihf@4.14.0:
|
/@rollup/rollup-linux-arm-gnueabihf@4.14.1:
|
||||||
resolution: {integrity: sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==}
|
resolution: {integrity: sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-arm64-gnu@4.14.0:
|
/@rollup/rollup-linux-arm64-gnu@4.14.1:
|
||||||
resolution: {integrity: sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==}
|
resolution: {integrity: sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-arm64-musl@4.14.0:
|
/@rollup/rollup-linux-arm64-musl@4.14.1:
|
||||||
resolution: {integrity: sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==}
|
resolution: {integrity: sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-powerpc64le-gnu@4.14.0:
|
/@rollup/rollup-linux-powerpc64le-gnu@4.14.1:
|
||||||
resolution: {integrity: sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==}
|
resolution: {integrity: sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==}
|
||||||
cpu: [ppc64le]
|
cpu: [ppc64le]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-riscv64-gnu@4.14.0:
|
/@rollup/rollup-linux-riscv64-gnu@4.14.1:
|
||||||
resolution: {integrity: sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==}
|
resolution: {integrity: sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-s390x-gnu@4.14.0:
|
/@rollup/rollup-linux-s390x-gnu@4.14.1:
|
||||||
resolution: {integrity: sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==}
|
resolution: {integrity: sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-x64-gnu@4.14.0:
|
/@rollup/rollup-linux-x64-gnu@4.14.1:
|
||||||
resolution: {integrity: sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==}
|
resolution: {integrity: sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-linux-x64-musl@4.14.0:
|
/@rollup/rollup-linux-x64-musl@4.14.1:
|
||||||
resolution: {integrity: sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==}
|
resolution: {integrity: sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-win32-arm64-msvc@4.14.0:
|
/@rollup/rollup-win32-arm64-msvc@4.14.1:
|
||||||
resolution: {integrity: sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==}
|
resolution: {integrity: sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-win32-ia32-msvc@4.14.0:
|
/@rollup/rollup-win32-ia32-msvc@4.14.1:
|
||||||
resolution: {integrity: sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==}
|
resolution: {integrity: sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/@rollup/rollup-win32-x64-msvc@4.14.0:
|
/@rollup/rollup-win32-x64-msvc@4.14.1:
|
||||||
resolution: {integrity: sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==}
|
resolution: {integrity: sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@ -675,19 +682,23 @@ packages:
|
|||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/eslint@8.56.7:
|
/@types/eslint@8.40.0:
|
||||||
resolution: {integrity: sha512-SjDvI/x3zsZnOkYZ3lCt9lOZWZLB2jIlNKz+LBgCtDurK0JZcwucxYHn1w2BJkD34dgX9Tjnak0txtq4WTggEA==}
|
resolution: {integrity: sha512-nbq2mvc/tBrK9zQQuItvjJl++GTN5j06DaPtp3hZCpngmG6Q3xoyEmd0TwZI0gAy/G1X0zhGBbr2imsGFdFV0g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.5
|
'@types/estree': 1.0.1
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.12
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/estree@1.0.1:
|
||||||
|
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/estree@1.0.5:
|
/@types/estree@1.0.5:
|
||||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@types/json-schema@7.0.15:
|
/@types/json-schema@7.0.12:
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@ungap/structured-clone@1.2.0:
|
/@ungap/structured-clone@1.2.0:
|
||||||
@ -739,6 +750,10 @@ packages:
|
|||||||
'@vue/compiler-dom': 3.4.21
|
'@vue/compiler-dom': 3.4.21
|
||||||
'@vue/shared': 3.4.21
|
'@vue/shared': 3.4.21
|
||||||
|
|
||||||
|
/@vue/devtools-api@6.5.0:
|
||||||
|
resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@vue/devtools-api@6.6.1:
|
/@vue/devtools-api@6.6.1:
|
||||||
resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
|
resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -823,8 +838,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/binary-extensions@2.3.0:
|
/binary-extensions@2.2.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -865,8 +880,8 @@ packages:
|
|||||||
'@kurkle/color': 0.3.2
|
'@kurkle/color': 0.3.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/chokidar@3.6.0:
|
/chokidar@3.5.3:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
anymatch: 3.1.3
|
anymatch: 3.1.3
|
||||||
@ -1136,9 +1151,9 @@ packages:
|
|||||||
file-entry-cache: 6.0.1
|
file-entry-cache: 6.0.1
|
||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
glob-parent: 6.0.2
|
glob-parent: 6.0.2
|
||||||
globals: 13.24.0
|
globals: 13.20.0
|
||||||
graphemer: 1.4.0
|
graphemer: 1.4.0
|
||||||
ignore: 5.3.1
|
ignore: 5.2.4
|
||||||
imurmurhash: 0.1.4
|
imurmurhash: 0.1.4
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
is-path-inside: 3.0.3
|
is-path-inside: 3.0.3
|
||||||
@ -1203,8 +1218,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/fastq@1.17.1:
|
/fastq@1.15.0:
|
||||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.0.4
|
reusify: 1.0.4
|
||||||
dev: true
|
dev: true
|
||||||
@ -1213,7 +1228,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 3.2.0
|
flat-cache: 3.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/fill-range@7.0.1:
|
/fill-range@7.0.1:
|
||||||
@ -1231,29 +1246,24 @@ packages:
|
|||||||
path-exists: 4.0.0
|
path-exists: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/flat-cache@3.2.0:
|
/flat-cache@3.0.4:
|
||||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
flatted: 3.3.1
|
flatted: 3.2.7
|
||||||
keyv: 4.5.4
|
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/flatted@3.3.1:
|
/flatted@3.2.7:
|
||||||
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
|
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/floating-vue@2.0.0(vue@3.4.21):
|
/floating-vue@2.0.0-beta.20(vue@3.4.21):
|
||||||
resolution: {integrity: sha512-YSffLYOjoaaPPBZc7VQR2qMCQ7xeXuh7i8a2u8WOdSmkjTtKtZpj2aaJnLtZRHmehrMHyCgtSxLu8jFNNX2sVw==}
|
resolution: {integrity: sha512-N68otcpp6WwcYC7zP8GeJqNZVdfvS7tEY88lwmuAHeqRgnfWx1Un8enzLxROyVnBDZ3TwUoUdj5IFg+bUT7JeA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nuxt/kit': ^3.2.0
|
|
||||||
vue: ^3.2.0
|
vue: ^3.2.0
|
||||||
peerDependenciesMeta:
|
|
||||||
'@nuxt/kit':
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/dom': 1.1.1
|
'@floating-ui/dom': 0.1.10
|
||||||
vue: 3.4.21
|
vue: 3.4.21
|
||||||
vue-resize: 2.0.0-alpha.1(vue@3.4.21)
|
vue-resize: 2.0.0-alpha.1(vue@3.4.21)
|
||||||
dev: false
|
dev: false
|
||||||
@ -1309,6 +1319,13 @@ packages:
|
|||||||
path-is-absolute: 1.0.1
|
path-is-absolute: 1.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/globals@13.20.0:
|
||||||
|
resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
type-fest: 0.20.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/globals@13.24.0:
|
/globals@13.24.0:
|
||||||
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
|
resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -1325,18 +1342,18 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/highlight.js@11.9.0:
|
/highlight.js@11.8.0:
|
||||||
resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==}
|
resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ignore@5.3.1:
|
/ignore@5.2.4:
|
||||||
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
|
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/immutable@4.3.5:
|
/immutable@4.3.0:
|
||||||
resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==}
|
resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/import-fresh@3.3.0:
|
/import-fresh@3.3.0:
|
||||||
@ -1367,7 +1384,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions: 2.3.0
|
binary-extensions: 2.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/is-extglob@2.1.1:
|
/is-extglob@2.1.1:
|
||||||
@ -1403,10 +1420,6 @@ packages:
|
|||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/json-buffer@3.0.1:
|
|
||||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/json-schema-traverse@0.4.1:
|
/json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1415,12 +1428,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/keyv@4.5.4:
|
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
|
||||||
dependencies:
|
|
||||||
json-buffer: 3.0.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/levn@0.4.1:
|
/levn@0.4.1:
|
||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -1463,8 +1470,8 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|
||||||
/markdown-it@13.0.2:
|
/markdown-it@13.0.1:
|
||||||
resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==}
|
resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
@ -1540,17 +1547,16 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chart.js: 4.4.2
|
chart.js: 4.4.2
|
||||||
dayjs: 1.11.10
|
dayjs: 1.11.10
|
||||||
floating-vue: 2.0.0(vue@3.4.21)
|
floating-vue: 2.0.0-beta.20(vue@3.4.21)
|
||||||
highlight.js: 11.9.0
|
highlight.js: 11.8.0
|
||||||
markdown-it: 13.0.2
|
markdown-it: 13.0.1
|
||||||
qrcode.vue: 3.4.1(vue@3.4.21)
|
qrcode.vue: 3.4.0(vue@3.4.21)
|
||||||
vue: 3.4.21
|
vue: 3.4.21
|
||||||
vue-chartjs: 5.3.0(chart.js@4.4.2)(vue@3.4.21)
|
vue-chartjs: 5.3.1(chart.js@4.4.2)(vue@3.4.21)
|
||||||
vue-router: 4.3.0(vue@3.4.21)
|
vue-router: 4.3.0(vue@3.4.21)
|
||||||
vue-select: 4.0.0-beta.6(vue@3.4.21)
|
vue-select: 4.0.0-beta.6(vue@3.4.21)
|
||||||
xss: 1.0.15
|
xss: 1.0.14
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
|
||||||
- typescript
|
- typescript
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -1628,9 +1634,9 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.1
|
'@vue/devtools-api': 6.5.0
|
||||||
vue: 3.4.21
|
vue: 3.4.21
|
||||||
vue-demi: 0.14.7(vue@3.4.21)
|
vue-demi: 0.14.5(vue@3.4.21)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/postcss-selector-parser@6.0.16:
|
/postcss-selector-parser@6.0.16:
|
||||||
@ -1660,13 +1666,13 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/punycode@2.3.1:
|
/punycode@2.3.0:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/qrcode.vue@3.4.1(vue@3.4.21):
|
/qrcode.vue@3.4.0(vue@3.4.21):
|
||||||
resolution: {integrity: sha512-wq/zHsifH4FJ1GXQi8/wNxD1KfQkckIpjK1KPTc/qwYU5/Bkd4me0w4xZSg6EXk6xLBkVDE0zxVagewv5EMAVA==}
|
resolution: {integrity: sha512-4XeImbv10Fin16Fl2DArCMhGyAdvIg2jb7vDT+hZiIAMg/6H6mz9nUZr/dR8jBcun5VzNzkiwKhiqOGbloinwA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.0
|
vue: ^3.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1709,28 +1715,28 @@ packages:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/rollup@4.14.0:
|
/rollup@4.14.1:
|
||||||
resolution: {integrity: sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==}
|
resolution: {integrity: sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.5
|
'@types/estree': 1.0.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@rollup/rollup-android-arm-eabi': 4.14.0
|
'@rollup/rollup-android-arm-eabi': 4.14.1
|
||||||
'@rollup/rollup-android-arm64': 4.14.0
|
'@rollup/rollup-android-arm64': 4.14.1
|
||||||
'@rollup/rollup-darwin-arm64': 4.14.0
|
'@rollup/rollup-darwin-arm64': 4.14.1
|
||||||
'@rollup/rollup-darwin-x64': 4.14.0
|
'@rollup/rollup-darwin-x64': 4.14.1
|
||||||
'@rollup/rollup-linux-arm-gnueabihf': 4.14.0
|
'@rollup/rollup-linux-arm-gnueabihf': 4.14.1
|
||||||
'@rollup/rollup-linux-arm64-gnu': 4.14.0
|
'@rollup/rollup-linux-arm64-gnu': 4.14.1
|
||||||
'@rollup/rollup-linux-arm64-musl': 4.14.0
|
'@rollup/rollup-linux-arm64-musl': 4.14.1
|
||||||
'@rollup/rollup-linux-powerpc64le-gnu': 4.14.0
|
'@rollup/rollup-linux-powerpc64le-gnu': 4.14.1
|
||||||
'@rollup/rollup-linux-riscv64-gnu': 4.14.0
|
'@rollup/rollup-linux-riscv64-gnu': 4.14.1
|
||||||
'@rollup/rollup-linux-s390x-gnu': 4.14.0
|
'@rollup/rollup-linux-s390x-gnu': 4.14.1
|
||||||
'@rollup/rollup-linux-x64-gnu': 4.14.0
|
'@rollup/rollup-linux-x64-gnu': 4.14.1
|
||||||
'@rollup/rollup-linux-x64-musl': 4.14.0
|
'@rollup/rollup-linux-x64-musl': 4.14.1
|
||||||
'@rollup/rollup-win32-arm64-msvc': 4.14.0
|
'@rollup/rollup-win32-arm64-msvc': 4.14.1
|
||||||
'@rollup/rollup-win32-ia32-msvc': 4.14.0
|
'@rollup/rollup-win32-ia32-msvc': 4.14.1
|
||||||
'@rollup/rollup-win32-x64-msvc': 4.14.0
|
'@rollup/rollup-win32-x64-msvc': 4.14.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
@ -1745,9 +1751,9 @@ packages:
|
|||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.5.3
|
||||||
immutable: 4.3.5
|
immutable: 4.3.0
|
||||||
source-map-js: 1.2.0
|
source-map-js: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/semver@7.6.0:
|
/semver@7.6.0:
|
||||||
@ -1775,6 +1781,11 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/source-map-js@1.0.2:
|
||||||
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/source-map-js@1.2.0:
|
/source-map-js@1.2.0:
|
||||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -1798,8 +1809,8 @@ packages:
|
|||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/svgo@3.2.0:
|
/svgo@3.0.2:
|
||||||
resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==}
|
resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1807,7 +1818,6 @@ packages:
|
|||||||
commander: 7.2.0
|
commander: 7.2.0
|
||||||
css-select: 5.1.0
|
css-select: 5.1.0
|
||||||
css-tree: 2.3.1
|
css-tree: 2.3.1
|
||||||
css-what: 6.1.0
|
|
||||||
csso: 5.0.5
|
csso: 5.0.5
|
||||||
picocolors: 1.0.0
|
picocolors: 1.0.0
|
||||||
dev: false
|
dev: false
|
||||||
@ -1850,7 +1860,7 @@ packages:
|
|||||||
/uri-js@4.4.1:
|
/uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
@ -1864,7 +1874,7 @@ packages:
|
|||||||
vite: '>=2'
|
vite: '>=2'
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
'@types/eslint': 8.56.7
|
'@types/eslint': 8.40.0
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
rollup: 2.79.1
|
rollup: 2.79.1
|
||||||
vite: 5.2.8(sass@1.74.1)
|
vite: 5.2.8(sass@1.74.1)
|
||||||
@ -1875,7 +1885,7 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: '>=3.2.13'
|
vue: '>=3.2.13'
|
||||||
dependencies:
|
dependencies:
|
||||||
svgo: 3.2.0
|
svgo: 3.0.2
|
||||||
vue: 3.4.21
|
vue: 3.4.21
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
@ -1909,14 +1919,14 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.20.2
|
esbuild: 0.20.2
|
||||||
postcss: 8.4.38
|
postcss: 8.4.38
|
||||||
rollup: 4.14.0
|
rollup: 4.14.1
|
||||||
sass: 1.74.1
|
sass: 1.74.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vue-chartjs@5.3.0(chart.js@4.4.2)(vue@3.4.21):
|
/vue-chartjs@5.3.1(chart.js@4.4.2)(vue@3.4.21):
|
||||||
resolution: {integrity: sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==}
|
resolution: {integrity: sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
chart.js: ^4.1.1
|
chart.js: ^4.1.1
|
||||||
vue: ^3.0.0-0 || ^2.7.0
|
vue: ^3.0.0-0 || ^2.7.0
|
||||||
@ -1925,8 +1935,8 @@ packages:
|
|||||||
vue: 3.4.21
|
vue: 3.4.21
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/vue-demi@0.14.7(vue@3.4.21):
|
/vue-demi@0.14.5(vue@3.4.21):
|
||||||
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
|
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@ -2038,8 +2048,8 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/xss@1.0.15:
|
/xss@1.0.14:
|
||||||
resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==}
|
resolution: {integrity: sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
use crate::api::Result;
|
use crate::api::Result;
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
use theseus::{hydra::init::DeviceLoginSuccess, prelude::*};
|
use tauri::Manager;
|
||||||
|
use theseus::prelude::*;
|
||||||
|
|
||||||
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||||
tauri::plugin::Builder::new("auth")
|
tauri::plugin::Builder::new("auth")
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
auth_authenticate_begin_flow,
|
auth_get_default_user,
|
||||||
auth_authenticate_await_completion,
|
auth_set_default_user,
|
||||||
auth_cancel_flow,
|
|
||||||
auth_refresh,
|
|
||||||
auth_remove_user,
|
auth_remove_user,
|
||||||
auth_has_user,
|
|
||||||
auth_users,
|
auth_users,
|
||||||
auth_get_user,
|
auth_get_user,
|
||||||
])
|
])
|
||||||
@ -20,47 +19,73 @@ pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
|||||||
/// Authenticate a user with Hydra - part 1
|
/// Authenticate a user with Hydra - part 1
|
||||||
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
|
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auth_authenticate_begin_flow() -> Result<DeviceLoginSuccess> {
|
pub async fn auth_login(app: tauri::AppHandle) -> Result<Option<Credentials>> {
|
||||||
Ok(auth::authenticate_begin_flow().await?)
|
let flow = minecraft_auth::begin_login().await?;
|
||||||
}
|
|
||||||
|
|
||||||
/// Authenticate a user with Hydra - part 2
|
let start = Utc::now();
|
||||||
/// This completes the authentication flow quasi-synchronously, returning the sign-in credentials
|
|
||||||
/// (and also adding the credentials to the state)
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn auth_authenticate_await_completion() -> Result<Credentials> {
|
|
||||||
Ok(auth::authenticate_await_complete_flow().await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
if let Some(window) = app.get_window("signin") {
|
||||||
pub async fn auth_cancel_flow() -> Result<()> {
|
window.close()?;
|
||||||
Ok(auth::cancel_flow().await?)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Refresh some credentials using Hydra, if needed
|
let window = tauri::WindowBuilder::new(
|
||||||
// invoke('plugin:auth|auth_refresh',user)
|
&app,
|
||||||
#[tauri::command]
|
"signin",
|
||||||
pub async fn auth_refresh(user: uuid::Uuid) -> Result<Credentials> {
|
tauri::WindowUrl::External(flow.redirect_uri.parse().map_err(
|
||||||
Ok(auth::refresh(user).await?)
|
|_| {
|
||||||
}
|
theseus::ErrorKind::OtherError(
|
||||||
|
"Error parsing auth redirect URL".to_string(),
|
||||||
|
)
|
||||||
|
.as_error()
|
||||||
|
},
|
||||||
|
)?),
|
||||||
|
)
|
||||||
|
.title("Sign into Modrinth")
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
while (Utc::now() - start) < Duration::minutes(10) {
|
||||||
|
if window
|
||||||
|
.url()
|
||||||
|
.as_str()
|
||||||
|
.starts_with("https://login.live.com/oauth20_desktop.srf")
|
||||||
|
{
|
||||||
|
if let Some((_, code)) =
|
||||||
|
window.url().query_pairs().find(|x| x.0 == "code")
|
||||||
|
{
|
||||||
|
window.close()?;
|
||||||
|
let val =
|
||||||
|
minecraft_auth::finish_login(&code.clone(), flow).await?;
|
||||||
|
|
||||||
|
return Ok(Some(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.close()?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auth_remove_user(user: uuid::Uuid) -> Result<()> {
|
pub async fn auth_remove_user(user: uuid::Uuid) -> Result<()> {
|
||||||
Ok(auth::remove_user(user).await?)
|
Ok(minecraft_auth::remove_user(user).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a user exists in Theseus
|
|
||||||
// invoke('plugin:auth|auth_has_user',user)
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auth_has_user(user: uuid::Uuid) -> Result<bool> {
|
pub async fn auth_get_default_user() -> Result<Option<uuid::Uuid>> {
|
||||||
Ok(auth::has_user(user).await?)
|
Ok(minecraft_auth::get_default_user().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn auth_set_default_user(user: uuid::Uuid) -> Result<()> {
|
||||||
|
Ok(minecraft_auth::set_default_user(user).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a copy of the list of all user credentials
|
/// Get a copy of the list of all user credentials
|
||||||
// invoke('plugin:auth|auth_users',user)
|
// invoke('plugin:auth|auth_users',user)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auth_users() -> Result<Vec<Credentials>> {
|
pub async fn auth_users() -> Result<Vec<Credentials>> {
|
||||||
Ok(auth::users().await?)
|
Ok(minecraft_auth::users().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a user from the UUID
|
/// Get a user from the UUID
|
||||||
@ -68,5 +93,5 @@ pub async fn auth_users() -> Result<Vec<Credentials>> {
|
|||||||
// invoke('plugin:auth|auth_users',user)
|
// invoke('plugin:auth|auth_users',user)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auth_get_user(user: uuid::Uuid) -> Result<Credentials> {
|
pub async fn auth_get_user(user: uuid::Uuid) -> Result<Credentials> {
|
||||||
Ok(auth::get_user(user).await?)
|
Ok(minecraft_auth::get_user(user).await?)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,9 @@ pub enum TheseusSerializableError {
|
|||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
IO(#[from] std::io::Error),
|
IO(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Tauri error: {0}")]
|
||||||
|
Tauri(#[from] tauri::Error),
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[error("Callback error: {0}")]
|
#[error("Callback error: {0}")]
|
||||||
Callback(String),
|
Callback(String),
|
||||||
@ -88,9 +91,12 @@ macro_rules! impl_serialize {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
impl_serialize! {
|
impl_serialize! {
|
||||||
IO,
|
IO,
|
||||||
|
Tauri,
|
||||||
Callback
|
Callback
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
impl_serialize! {
|
impl_serialize! {
|
||||||
IO,
|
IO,
|
||||||
|
Tauri,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,6 +146,7 @@ fn main() {
|
|||||||
initialize_state,
|
initialize_state,
|
||||||
is_dev,
|
is_dev,
|
||||||
toggle_decorations,
|
toggle_decorations,
|
||||||
|
api::auth::auth_login,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { get } from '@/helpers/settings'
|
|||||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||||
|
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
||||||
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
|
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator'
|
||||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||||
import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
|
import { offline_listener, command_listener, warning_listener } from '@/helpers/events.js'
|
||||||
@ -40,15 +41,14 @@ import { TauriEvent } from '@tauri-apps/api/event'
|
|||||||
import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
|
import { await_sync, check_safe_loading_bars_complete } from './helpers/state'
|
||||||
import { confirm } from '@tauri-apps/api/dialog'
|
import { confirm } from '@tauri-apps/api/dialog'
|
||||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||||
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
|
|
||||||
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
|
import OnboardingScreen from '@/components/ui/tutorial/OnboardingScreen.vue'
|
||||||
import { install_from_file } from './helpers/pack'
|
import { install_from_file } from './helpers/pack'
|
||||||
|
import { useError } from '@/store/error.js'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
const urlModal = ref(null)
|
const urlModal = ref(null)
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
|
|
||||||
const videoPlaying = ref(false)
|
|
||||||
const offline = ref(false)
|
const offline = ref(false)
|
||||||
const showOnboarding = ref(false)
|
const showOnboarding = ref(false)
|
||||||
const nativeDecorations = ref(false)
|
const nativeDecorations = ref(false)
|
||||||
@ -71,7 +71,6 @@ defineExpose({
|
|||||||
} = await get()
|
} = await get()
|
||||||
// video should play if the user is not on linux, and has not onboarded
|
// video should play if the user is not on linux, and has not onboarded
|
||||||
os.value = await getOS()
|
os.value = await getOS()
|
||||||
videoPlaying.value = !fully_onboarded && os.value !== 'Linux'
|
|
||||||
const dev = await isDev()
|
const dev = await isDev()
|
||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
showOnboarding.value = !fully_onboarded
|
showOnboarding.value = !fully_onboarded
|
||||||
@ -180,12 +179,19 @@ const isOnBrowse = computed(() => route.path.startsWith('/browse'))
|
|||||||
const loading = useLoading()
|
const loading = useLoading()
|
||||||
|
|
||||||
const notifications = useNotifications()
|
const notifications = useNotifications()
|
||||||
const notificationsWrapper = ref(null)
|
const notificationsWrapper = ref()
|
||||||
|
|
||||||
watch(notificationsWrapper, () => {
|
watch(notificationsWrapper, () => {
|
||||||
notifications.setNotifs(notificationsWrapper.value)
|
notifications.setNotifs(notificationsWrapper.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const error = useError()
|
||||||
|
const errorModal = ref()
|
||||||
|
|
||||||
|
watch(errorModal, () => {
|
||||||
|
error.setErrorModal(errorModal.value)
|
||||||
|
})
|
||||||
|
|
||||||
document.querySelector('body').addEventListener('click', function (e) {
|
document.querySelector('body').addEventListener('click', function (e) {
|
||||||
let target = e.target
|
let target = e.target
|
||||||
while (target != null) {
|
while (target != null) {
|
||||||
@ -245,15 +251,6 @@ command_listener(async (e) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<StickyTitleBar v-if="videoPlaying" />
|
|
||||||
<video
|
|
||||||
v-if="videoPlaying"
|
|
||||||
ref="onboardingVideo"
|
|
||||||
class="video"
|
|
||||||
src="@/assets/video.mp4"
|
|
||||||
autoplay
|
|
||||||
@ended="videoPlaying = false"
|
|
||||||
/>
|
|
||||||
<div v-if="failureText" class="failure dark-mode">
|
<div v-if="failureText" class="failure dark-mode">
|
||||||
<div class="appbar-failure dark-mode">
|
<div class="appbar-failure dark-mode">
|
||||||
<Button v-if="os != 'MacOS'" icon-only @click="TauriWindow.getCurrent().close()">
|
<Button v-if="os != 'MacOS'" icon-only @click="TauriWindow.getCurrent().close()">
|
||||||
@ -294,7 +291,7 @@ command_listener(async (e) => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SplashScreen v-else-if="!videoPlaying && isLoading" app-loading />
|
<SplashScreen v-else-if="isLoading" app-loading />
|
||||||
<OnboardingScreen v-else-if="showOnboarding" :finish="() => (showOnboarding = false)" />
|
<OnboardingScreen v-else-if="showOnboarding" :finish="() => (showOnboarding = false)" />
|
||||||
<div v-else class="container">
|
<div v-else class="container">
|
||||||
<div class="nav-container">
|
<div class="nav-container">
|
||||||
@ -389,6 +386,7 @@ command_listener(async (e) => {
|
|||||||
</div>
|
</div>
|
||||||
<URLConfirmModal ref="urlModal" />
|
<URLConfirmModal ref="urlModal" />
|
||||||
<Notifications ref="notificationsWrapper" />
|
<Notifications ref="notificationsWrapper" />
|
||||||
|
<ErrorModal ref="errorModal" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
Binary file not shown.
@ -56,68 +56,22 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</transition>
|
</transition>
|
||||||
<Modal ref="loginModal" class="modal" header="Signing in" :noblur="!themeStore.advancedRendering">
|
|
||||||
<div class="modal-body">
|
|
||||||
<QrcodeVue :value="loginUrl" class="qr-code" margin="3" size="160" />
|
|
||||||
<div class="modal-text">
|
|
||||||
<div class="label">Copy this code</div>
|
|
||||||
<div class="code-text">
|
|
||||||
<div class="code">
|
|
||||||
{{ loginCode }}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
v-tooltip="'Copy code'"
|
|
||||||
icon-only
|
|
||||||
large
|
|
||||||
color="raised"
|
|
||||||
@click="() => clipboardWrite(loginCode)"
|
|
||||||
>
|
|
||||||
<ClipboardCopyIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>And enter it on Microsoft's website to sign in.</div>
|
|
||||||
<div class="iconified-input">
|
|
||||||
<LogInIcon />
|
|
||||||
<input type="text" :value="loginUrl" readonly />
|
|
||||||
<Button
|
|
||||||
v-tooltip="'Open link'"
|
|
||||||
icon-only
|
|
||||||
color="raised"
|
|
||||||
@click="() => clipboardWrite(loginUrl)"
|
|
||||||
>
|
|
||||||
<GlobeIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { Avatar, Button, Card, PlusIcon, TrashIcon, LogInIcon } from 'omorphia'
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
PlusIcon,
|
|
||||||
TrashIcon,
|
|
||||||
LogInIcon,
|
|
||||||
Modal,
|
|
||||||
GlobeIcon,
|
|
||||||
ClipboardCopyIcon,
|
|
||||||
} from 'omorphia'
|
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
users,
|
users,
|
||||||
remove_user,
|
remove_user,
|
||||||
authenticate_begin_flow,
|
set_default_user,
|
||||||
authenticate_await_completion,
|
login as login_flow,
|
||||||
|
get_default_user,
|
||||||
} from '@/helpers/auth'
|
} from '@/helpers/auth'
|
||||||
import { get, set } from '@/helpers/settings'
|
|
||||||
import { handleError } from '@/store/state.js'
|
import { handleError } from '@/store/state.js'
|
||||||
import { useTheming } from '@/store/theme.js'
|
|
||||||
import { mixpanel_track } from '@/helpers/mixpanel'
|
import { mixpanel_track } from '@/helpers/mixpanel'
|
||||||
import QrcodeVue from 'qrcode.vue'
|
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
@ -129,16 +83,11 @@ defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['change'])
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
const loginCode = ref(null)
|
const accounts = ref({})
|
||||||
|
const defaultUser = ref()
|
||||||
const themeStore = useTheming()
|
|
||||||
const settings = ref({})
|
|
||||||
const accounts = ref([])
|
|
||||||
const loginUrl = ref('')
|
|
||||||
const loginModal = ref(null)
|
|
||||||
|
|
||||||
async function refreshValues() {
|
async function refreshValues() {
|
||||||
settings.value = await get().catch(handleError)
|
defaultUser.value = await get_default_user().catch(handleError)
|
||||||
accounts.value = await users().catch(handleError)
|
accounts.value = await users().catch(handleError)
|
||||||
}
|
}
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@ -147,46 +96,27 @@ defineExpose({
|
|||||||
await refreshValues()
|
await refreshValues()
|
||||||
|
|
||||||
const displayAccounts = computed(() =>
|
const displayAccounts = computed(() =>
|
||||||
accounts.value.filter((account) => settings.value.default_user !== account.id),
|
accounts.value.filter((account) => defaultUser.value !== account.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedAccount = computed(() =>
|
const selectedAccount = computed(() =>
|
||||||
accounts.value.find((account) => account.id === settings.value.default_user),
|
accounts.value.find((account) => account.id === defaultUser.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function setAccount(account) {
|
async function setAccount(account) {
|
||||||
settings.value.default_user = account.id
|
defaultUser.value = account.id
|
||||||
await set(settings.value).catch(handleError)
|
await set_default_user(account.id).catch(handleError)
|
||||||
emit('change')
|
emit('change')
|
||||||
}
|
}
|
||||||
|
|
||||||
const clipboardWrite = async (a) => {
|
|
||||||
navigator.clipboard.writeText(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
const loginSuccess = await authenticate_begin_flow().catch(handleError)
|
const loggedIn = await login_flow().catch(handleSevereError)
|
||||||
|
|
||||||
loginModal.value.show()
|
|
||||||
loginCode.value = loginSuccess.user_code
|
|
||||||
loginUrl.value = loginSuccess.verification_uri
|
|
||||||
await window.__TAURI_INVOKE__('tauri', {
|
|
||||||
__tauriModule: 'Shell',
|
|
||||||
message: {
|
|
||||||
cmd: 'open',
|
|
||||||
path: loginSuccess.verification_uri,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const loggedIn = await authenticate_await_completion().catch(handleError)
|
|
||||||
loginModal.value.hide()
|
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await setAccount(loggedIn)
|
await setAccount(loggedIn)
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
}
|
}
|
||||||
|
|
||||||
loginModal.value.hide()
|
|
||||||
mixpanel_track('AccountLogIn')
|
mixpanel_track('AccountLogIn')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
125
theseus_gui/src/components/ui/ErrorModal.vue
Normal file
125
theseus_gui/src/components/ui/ErrorModal.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Modal, XIcon } from 'omorphia'
|
||||||
|
import { ChatIcon } from '@/assets/icons'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const errorModal = ref()
|
||||||
|
const error = ref()
|
||||||
|
|
||||||
|
const title = ref('An error occurred')
|
||||||
|
const errorType = ref('unknown')
|
||||||
|
const supportLink = ref('https://support.modrinth.com')
|
||||||
|
const metadata = ref({})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
async show(errorVal) {
|
||||||
|
if (errorVal.message && errorVal.message.includes('Minecraft authentication error:')) {
|
||||||
|
title.value = 'Unable to sign in to Minecraft'
|
||||||
|
errorType.value = 'minecraft_auth'
|
||||||
|
supportLink.value =
|
||||||
|
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
|
||||||
|
|
||||||
|
if (errorVal.message.includes('existing connection was forcibly closed')) {
|
||||||
|
metadata.value.network = true
|
||||||
|
}
|
||||||
|
if (errorVal.message.includes('because the target machine actively refused it')) {
|
||||||
|
metadata.value.hostsFile = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title.value = 'An error occurred'
|
||||||
|
errorType.value = 'unknown'
|
||||||
|
supportLink.value = 'https://support.modrinth.com'
|
||||||
|
metadata.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
error.value = errorVal
|
||||||
|
errorModal.value.show()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal ref="errorModal" :header="title">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="markdown-body">
|
||||||
|
<template v-if="errorType === 'minecraft_auth'">
|
||||||
|
<p>
|
||||||
|
Signing into Microsoft account is a complex task for the launchers, and there are a lot
|
||||||
|
of things can go wrong.
|
||||||
|
</p>
|
||||||
|
<template v-if="metadata.network">
|
||||||
|
<h3>Network issues</h3>
|
||||||
|
<p>
|
||||||
|
It looks like there were issues with the Modrinth App connecting to Microsoft's
|
||||||
|
servers. This is often the result of a poor connection, so we recommend trying again
|
||||||
|
to see if it works. If issues continue to persist, follow the steps in
|
||||||
|
<a
|
||||||
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_e71a5f805f"
|
||||||
|
>
|
||||||
|
our support article
|
||||||
|
</a>
|
||||||
|
to troubleshoot.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="metadata.hostsFile">
|
||||||
|
<h3>Network issues</h3>
|
||||||
|
<p>
|
||||||
|
The Modrinth App tried to connect to Microsoft / Xbox / Minecraft services, but the
|
||||||
|
remote server rejected the connection. This may indicate that these services are
|
||||||
|
blocked by the hosts file. Please visit
|
||||||
|
<a
|
||||||
|
href="https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues#h_d694a29256"
|
||||||
|
>
|
||||||
|
our support article
|
||||||
|
</a>
|
||||||
|
for steps on how to fix the issue.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h3>Make sure you are signing into the right Microsoft account</h3>
|
||||||
|
<p>
|
||||||
|
More often than not, this error is caused by you signing into an incorrect Microsoft
|
||||||
|
account which isn't linked to Minecraft. Double check and try again!
|
||||||
|
</p>
|
||||||
|
<h3>Try signing in and launching through the official launcher first</h3>
|
||||||
|
<p>
|
||||||
|
If you just bought Minecraft, are coming from the Bedrock Edition world and have never
|
||||||
|
played Java before, or just subscribed to PC Game Pass, you would need to start the
|
||||||
|
game at least once using the
|
||||||
|
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>.
|
||||||
|
Once you're done, come back here and sign in!
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<p>
|
||||||
|
If nothing is working and you need help, visit
|
||||||
|
<a :href="supportLink">our support page</a>
|
||||||
|
and start a chat using the widget in the bottom right and we will be more than happy to
|
||||||
|
assist!
|
||||||
|
</p>
|
||||||
|
<details>
|
||||||
|
<summary>Debug info</summary>
|
||||||
|
{{ error.message ?? error }}
|
||||||
|
</details>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ error.message ?? error }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="input-group push-right">
|
||||||
|
<a :href="supportLink" class="btn" @click="errorModal.hide()"><ChatIcon /> Get support</a>
|
||||||
|
<button class="btn btn-primary" @click="errorModal.hide()"><XIcon /> Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--gap-md);
|
||||||
|
padding: var(--gap-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="action-groups">
|
<div class="action-groups">
|
||||||
<a href="https://discord.modrinth.com" class="link">
|
<a href="https://support.modrinth.com" class="link">
|
||||||
<ChatIcon />
|
<ChatIcon />
|
||||||
<span> Get support </span>
|
<span> Get support </span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -1,178 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="button" class="button-base avatar-button" :class="{ highlighted: showDemo }">
|
|
||||||
<Avatar src="https://launcher-files.modrinth.com/assets/steve_head.png" />
|
|
||||||
</div>
|
|
||||||
<transition name="fade">
|
|
||||||
<div v-if="showDemo" class="card-section">
|
|
||||||
<Card ref="card" class="fake-account-card expanded highlighted">
|
|
||||||
<div class="selected account">
|
|
||||||
<Avatar size="xs" src="https://launcher-files.modrinth.com/assets/steve_head.png" />
|
|
||||||
<div>
|
|
||||||
<h4>Modrinth</h4>
|
|
||||||
<p>Selected</p>
|
|
||||||
</div>
|
|
||||||
<Button v-tooltip="'Log out'" icon-only color="raised">
|
|
||||||
<TrashIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button>
|
|
||||||
<PlusIcon />
|
|
||||||
Add account
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { Avatar, Button, Card, PlusIcon, TrashIcon } from 'omorphia'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
showDemo: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.selected {
|
|
||||||
background: var(--color-brand-highlight);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
color: var(--color-contrast);
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logged-out {
|
|
||||||
background: var(--color-bg);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account {
|
|
||||||
width: max-content;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
text-align: left;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
|
|
||||||
h4,
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-section {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
left: 5.5rem;
|
|
||||||
z-index: 9;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fake-account-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
width: max-content;
|
|
||||||
user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.isolated {
|
|
||||||
position: relative;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.accounts-title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-group {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option {
|
|
||||||
width: calc(100% - 2.25rem);
|
|
||||||
background: var(--color-raised-bg);
|
|
||||||
color: var(--color-base);
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
--size: 1.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
vertical-align: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: var(--color-base);
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&.expanded {
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-text {
|
|
||||||
margin: auto 0 auto 0.25rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
width: 6rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accounts-text {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,265 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="action-groups">
|
|
||||||
<Button color="danger" outline @click="exit">
|
|
||||||
<LogOutIcon />
|
|
||||||
Exit tutorial
|
|
||||||
</Button>
|
|
||||||
<Button v-if="showDownload" ref="infoButton" icon-only class="icon-button show-card-icon">
|
|
||||||
<DownloadIcon />
|
|
||||||
</Button>
|
|
||||||
<div v-if="showRunning" class="status highlighted">
|
|
||||||
<span class="circle running" />
|
|
||||||
<div ref="profileButton" class="running-text">Example Modpack</div>
|
|
||||||
<Button v-tooltip="'Stop instance'" icon-only class="icon-button stop">
|
|
||||||
<StopCircleIcon />
|
|
||||||
</Button>
|
|
||||||
<Button v-tooltip="'View logs'" icon-only class="icon-button">
|
|
||||||
<TerminalSquareIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="status">
|
|
||||||
<span class="circle stopped" />
|
|
||||||
<span class="running-text"> No running instances </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<transition name="download">
|
|
||||||
<div v-if="showDownload" class="info-section">
|
|
||||||
<Card ref="card" class="highlighted info-card">
|
|
||||||
<h3 class="info-title">New Modpack</h3>
|
|
||||||
<ProgressBar :progress="50" />
|
|
||||||
<div class="row">50% Downloading modpack</div>
|
|
||||||
</Card>
|
|
||||||
<slot name="download" />
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
<transition name="running">
|
|
||||||
<div v-if="showRunning" class="info-section">
|
|
||||||
<slot name="running" />
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DownloadIcon,
|
|
||||||
Card,
|
|
||||||
StopCircleIcon,
|
|
||||||
TerminalSquareIcon,
|
|
||||||
LogOutIcon,
|
|
||||||
} from 'omorphia'
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
showDownload: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
showRunning: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
exit: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.action-groups {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
transition: transform 0.2s ease-in-out;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
&.rotate {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.running-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--gap-xs);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-user-select: none; /* Safari */
|
|
||||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&.clickable:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle {
|
|
||||||
width: 0.5rem;
|
|
||||||
height: 0.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
|
|
||||||
&.running {
|
|
||||||
background-color: var(--color-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.stopped {
|
|
||||||
background-color: var(--color-base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
box-shadow: none;
|
|
||||||
width: 1.25rem !important;
|
|
||||||
height: 1.25rem !important;
|
|
||||||
|
|
||||||
&.stop {
|
|
||||||
--text-color: var(--color-red) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section {
|
|
||||||
position: absolute;
|
|
||||||
top: 3.5rem;
|
|
||||||
right: 0.75rem;
|
|
||||||
z-index: 9;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card {
|
|
||||||
width: 20rem;
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
box-shadow: var(--shadow-raised);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
overflow: auto;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
background-color: var(--color-raised-bg-hover);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-icon {
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
:deep(svg) {
|
|
||||||
left: 1rem;
|
|
||||||
width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-card-icon {
|
|
||||||
color: var(--color-brand);
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-enter-active,
|
|
||||||
.download-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-enter-from,
|
|
||||||
.download-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-title {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-card {
|
|
||||||
position: absolute;
|
|
||||||
top: 3.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
z-index: 9;
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
box-shadow: var(--shadow-raised);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: auto;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
padding: var(--gap-md);
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,222 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { Card, DropdownSelect, SearchIcon, XIcon, Button, Avatar } from 'omorphia'
|
|
||||||
|
|
||||||
const search = ref('')
|
|
||||||
const group = ref('Category')
|
|
||||||
const filters = ref('All profiles')
|
|
||||||
const sortBy = ref('Name')
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
showFilters: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
showInstances: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<Card class="header" :class="{ highlighted: showFilters }">
|
|
||||||
<div class="iconified-input">
|
|
||||||
<SearchIcon />
|
|
||||||
<input v-model="search" type="text" placeholder="Search" class="search-input" />
|
|
||||||
<Button @click="() => (search = '')">
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="labeled_button">
|
|
||||||
<span>Sort by</span>
|
|
||||||
<DropdownSelect
|
|
||||||
v-model="sortBy"
|
|
||||||
class="sort-dropdown"
|
|
||||||
name="Sort Dropdown"
|
|
||||||
:options="['Name', 'Last played', 'Date created', 'Date modified', 'Game version']"
|
|
||||||
placeholder="Select..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="labeled_button">
|
|
||||||
<span>Filter by</span>
|
|
||||||
<DropdownSelect
|
|
||||||
v-model="filters"
|
|
||||||
class="filter-dropdown"
|
|
||||||
name="Filter Dropdown"
|
|
||||||
:options="['All profiles', 'Custom instances', 'Downloaded modpacks']"
|
|
||||||
placeholder="Select..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="labeled_button">
|
|
||||||
<span>Group by</span>
|
|
||||||
<DropdownSelect
|
|
||||||
v-model="group"
|
|
||||||
class="group-dropdown"
|
|
||||||
name="Group dropdown"
|
|
||||||
:options="['Category', 'Loader', 'Game version', 'None']"
|
|
||||||
placeholder="Select..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<div class="row">
|
|
||||||
<section class="instances">
|
|
||||||
<Card
|
|
||||||
v-for="project in 20"
|
|
||||||
:key="project"
|
|
||||||
class="instance-card-item button-base"
|
|
||||||
:class="{ highlighted: project === 1 && showInstance }"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
size="sm"
|
|
||||||
src="https://launcher-files.modrinth.com/assets/default_profile.png"
|
|
||||||
alt="Mod card"
|
|
||||||
class="mod-image"
|
|
||||||
/>
|
|
||||||
<div class="project-info">
|
|
||||||
<p class="title">Example Profile</p>
|
|
||||||
<p class="description">Forge/Fabric 1.20.1</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<slot />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
background-color: var(--color-gray);
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: inherit;
|
|
||||||
margin: 1rem 1rem 0 !important;
|
|
||||||
padding: 1rem;
|
|
||||||
width: calc(100% - 2rem);
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
input {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-dropdown {
|
|
||||||
width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-dropdown {
|
|
||||||
width: 15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-dropdown {
|
|
||||||
width: 10rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.labeled_button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.instances {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
|
||||||
width: 100%;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-right: auto;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-card-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--gap-md);
|
|
||||||
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
.mod-image {
|
|
||||||
--size: 100%;
|
|
||||||
|
|
||||||
width: 100% !important;
|
|
||||||
height: auto !important;
|
|
||||||
max-width: unset !important;
|
|
||||||
max-height: unset !important;
|
|
||||||
aspect-ratio: 1 / 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-info {
|
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 110%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
color: var(--color-base);
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.775rem;
|
|
||||||
line-height: 125%;
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
text-transform: capitalize;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,376 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import {
|
|
||||||
DownloadIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
formatNumber,
|
|
||||||
CalendarIcon,
|
|
||||||
HeartIcon,
|
|
||||||
Avatar,
|
|
||||||
Card,
|
|
||||||
} from 'omorphia'
|
|
||||||
import { onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const modsRow = ref(null)
|
|
||||||
const rows = ref(null)
|
|
||||||
const maxInstancesPerRow = ref(0)
|
|
||||||
const maxProjectsPerRow = ref(0)
|
|
||||||
|
|
||||||
const calculateCardsPerRow = () => {
|
|
||||||
// Calculate how many cards fit in one row
|
|
||||||
const containerWidth = rows.value[0].clientWidth
|
|
||||||
// Convert container width from pixels to rem
|
|
||||||
const containerWidthInRem =
|
|
||||||
containerWidth / parseFloat(getComputedStyle(document.documentElement).fontSize)
|
|
||||||
maxInstancesPerRow.value = Math.floor((containerWidthInRem + 1) / 11)
|
|
||||||
maxProjectsPerRow.value = Math.floor((containerWidthInRem + 1) / 17)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
calculateCardsPerRow()
|
|
||||||
window.addEventListener('resize', calculateCardsPerRow)
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', calculateCardsPerRow)
|
|
||||||
})
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
showInstance: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="content">
|
|
||||||
<div
|
|
||||||
v-for="(row, index) in ['Jump back in', 'Popular modpacks', 'Popular mods']"
|
|
||||||
ref="rows"
|
|
||||||
:key="row"
|
|
||||||
class="row"
|
|
||||||
>
|
|
||||||
<div class="header">
|
|
||||||
<p>{{ row }}</p>
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</div>
|
|
||||||
<section v-if="index < 1" ref="modsRow" class="instances">
|
|
||||||
<Card
|
|
||||||
v-for="project in maxInstancesPerRow"
|
|
||||||
:key="project"
|
|
||||||
class="instance-card-item button-base"
|
|
||||||
:class="{ highlighted: showInstance }"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
size="sm"
|
|
||||||
src="https://launcher-files.modrinth.com/assets/default_profile.png"
|
|
||||||
alt="Mod card"
|
|
||||||
class="mod-image"
|
|
||||||
/>
|
|
||||||
<div class="project-info">
|
|
||||||
<p class="title">Example Profile</p>
|
|
||||||
<p class="description">Forge/Fabric 1.20.1</p>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
<section v-else ref="modsRow" class="projects">
|
|
||||||
<div v-for="project in maxProjectsPerRow" :key="project" class="wrapper">
|
|
||||||
<Card class="project-card button-base" :class="{ highlighted: showInstance }">
|
|
||||||
<div
|
|
||||||
class="banner no-image"
|
|
||||||
:style="{
|
|
||||||
'background-image': `url(https://launcher-files.modrinth.com/assets/maze-bg.png)`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="badges">
|
|
||||||
<div class="badge">
|
|
||||||
<DownloadIcon />
|
|
||||||
{{ formatNumber(69420) }}
|
|
||||||
</div>
|
|
||||||
<div class="badge">
|
|
||||||
<HeartIcon />
|
|
||||||
{{ formatNumber(69) }}
|
|
||||||
</div>
|
|
||||||
<div class="badge">
|
|
||||||
<CalendarIcon />
|
|
||||||
Today
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="badges-wrapper no-image"
|
|
||||||
:style="{
|
|
||||||
background:
|
|
||||||
'linear-gradient(rgba(' +
|
|
||||||
[27, 217, 106, 0.03].join(',') +
|
|
||||||
'), 65%, rgba(' +
|
|
||||||
[27, 217, 106, 0.3].join(',') +
|
|
||||||
'))',
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<Avatar
|
|
||||||
class="icon"
|
|
||||||
size="sm"
|
|
||||||
src="https://launcher-files.modrinth.com/assets/default_profile.png"
|
|
||||||
/>
|
|
||||||
<div class="title">
|
|
||||||
<div class="title-text">Example Project</div>
|
|
||||||
<div class="author">by Modrinth</div>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
An example project hangin on the Rinth. Very cool project, its probably on Forge and
|
|
||||||
Fabric. Probably has a 401k and a family.
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
padding: 1rem;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 100%;
|
|
||||||
|
|
||||||
&:nth-child(even) {
|
|
||||||
background: var(--color-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
gap: var(--gap-xs);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
font-weight: bolder;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
height: 1.5rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.instances {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
|
||||||
grid-gap: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
|
||||||
grid-gap: 1rem;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator {
|
|
||||||
width: 2.5rem !important;
|
|
||||||
height: 2.5rem !important;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 2.5rem !important;
|
|
||||||
height: 2.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-card-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--gap-md);
|
|
||||||
transition: 0.1s ease-in-out all !important; /* overrides Omorphia defaults */
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
.mod-image {
|
|
||||||
--size: 100%;
|
|
||||||
|
|
||||||
width: 100% !important;
|
|
||||||
height: auto !important;
|
|
||||||
max-width: unset !important;
|
|
||||||
max-height: unset !important;
|
|
||||||
aspect-ratio: 1 / 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-info {
|
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 110%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
color: var(--color-base);
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.775rem;
|
|
||||||
line-height: 125%;
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
text-transform: capitalize;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
aspect-ratio: 1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.install:enabled {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: 1rem;
|
|
||||||
grid-template:
|
|
||||||
'. . . .' 0
|
|
||||||
'. icon title .' 3rem
|
|
||||||
'banner banner banner banner' auto
|
|
||||||
'. description description .' 3.5rem
|
|
||||||
'. . . .' 0 / 0 3rem minmax(0, 1fr) 0;
|
|
||||||
max-width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
grid-area: icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
max-width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
grid-area: title;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.title-text {
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.author {
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
grid-area: author;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
grid-area: banner;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.badges-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
mix-blend-mode: hard-light;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badges {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--gap-sm);
|
|
||||||
gap: var(--gap-xs);
|
|
||||||
display: flex;
|
|
||||||
z-index: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
grid-area: description;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
background-color: var(--color-raised-bg);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
padding: var(--gap-xs) var(--gap-sm);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
margin-right: var(--gap-xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,496 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, readonly, ref } from 'vue'
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
CalendarIcon,
|
|
||||||
Card,
|
|
||||||
Categories,
|
|
||||||
Checkbox,
|
|
||||||
ClearIcon,
|
|
||||||
ClientIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
DropdownSelect,
|
|
||||||
EnvironmentIndicator,
|
|
||||||
formatCategory,
|
|
||||||
formatCategoryHeader,
|
|
||||||
formatNumber,
|
|
||||||
HeartIcon,
|
|
||||||
NavRow,
|
|
||||||
Pagination,
|
|
||||||
Promotion,
|
|
||||||
SearchFilter,
|
|
||||||
SearchIcon,
|
|
||||||
ServerIcon,
|
|
||||||
StarIcon,
|
|
||||||
XIcon,
|
|
||||||
} from 'omorphia'
|
|
||||||
import Multiselect from 'vue-multiselect'
|
|
||||||
import { handleError } from '@/store/state'
|
|
||||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
|
||||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const query = ref('')
|
|
||||||
const facets = ref([])
|
|
||||||
const orFacets = ref([])
|
|
||||||
const selectedVersions = ref([])
|
|
||||||
const onlyOpenSource = ref(false)
|
|
||||||
const showSnapshots = ref(false)
|
|
||||||
const selectedEnvironments = ref([])
|
|
||||||
const sortTypes = readonly([
|
|
||||||
{ display: 'Relevance', name: 'relevance' },
|
|
||||||
{ display: 'Download count', name: 'downloads' },
|
|
||||||
{ display: 'Follow count', name: 'follows' },
|
|
||||||
{ display: 'Recently published', name: 'newest' },
|
|
||||||
{ display: 'Recently updated', name: 'updated' },
|
|
||||||
])
|
|
||||||
const sortType = ref(sortTypes[0])
|
|
||||||
const maxResults = ref(20)
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const projectType = ref('modpack')
|
|
||||||
|
|
||||||
const searchWrapper = ref(null)
|
|
||||||
|
|
||||||
const sortedCategories = computed(() => {
|
|
||||||
const values = new Map()
|
|
||||||
for (const category of categories.value.filter((cat) => cat.project_type === 'mod')) {
|
|
||||||
if (!values.has(category.header)) {
|
|
||||||
values.set(category.header, [])
|
|
||||||
}
|
|
||||||
values.get(category.header).push(category)
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
})
|
|
||||||
|
|
||||||
const [categories, loaders, availableGameVersions] = await Promise.all([
|
|
||||||
get_categories().catch(handleError).then(ref),
|
|
||||||
get_loaders().catch(handleError).then(ref),
|
|
||||||
get_game_versions().catch(handleError).then(ref),
|
|
||||||
])
|
|
||||||
|
|
||||||
const pageCount = ref(1)
|
|
||||||
|
|
||||||
const selectableProjectTypes = computed(() => {
|
|
||||||
return [
|
|
||||||
{ label: 'Shaders', href: `` },
|
|
||||||
{ label: 'Resource Packs', href: `` },
|
|
||||||
{ label: 'Data Packs', href: `` },
|
|
||||||
{ label: 'Mods', href: '' },
|
|
||||||
{ label: 'Modpacks', href: '' },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
showSearch: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="search-container">
|
|
||||||
<aside class="filter-panel">
|
|
||||||
<Card class="search-panel-card" :class="{ highlighted: showSearch }">
|
|
||||||
<Button role="button" disabled> <ClearIcon /> Clear Filters </Button>
|
|
||||||
<div class="loaders">
|
|
||||||
<h2>Loaders</h2>
|
|
||||||
<div
|
|
||||||
v-for="loader in loaders.filter(
|
|
||||||
(l) =>
|
|
||||||
(projectType !== 'mod' && l.supported_project_types?.includes(projectType)) ||
|
|
||||||
(projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name)),
|
|
||||||
)"
|
|
||||||
:key="loader"
|
|
||||||
>
|
|
||||||
<SearchFilter
|
|
||||||
:active-filters="orFacets"
|
|
||||||
:icon="loader.icon"
|
|
||||||
:display-name="formatCategory(loader.name)"
|
|
||||||
:facet-name="`categories:${encodeURIComponent(loader.name)}`"
|
|
||||||
class="filter-checkbox"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="versions">
|
|
||||||
<h2>Minecraft versions</h2>
|
|
||||||
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
|
|
||||||
<multiselect
|
|
||||||
v-model="selectedVersions"
|
|
||||||
:options="
|
|
||||||
showSnapshots
|
|
||||||
? availableGameVersions.map((x) => x.version)
|
|
||||||
: availableGameVersions
|
|
||||||
.filter((it) => it.version_type === 'release')
|
|
||||||
.map((x) => x.version)
|
|
||||||
"
|
|
||||||
:multiple="true"
|
|
||||||
:searchable="true"
|
|
||||||
:show-no-results="false"
|
|
||||||
:close-on-select="false"
|
|
||||||
:clear-search-on-select="false"
|
|
||||||
:show-labels="false"
|
|
||||||
placeholder="Choose versions..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="categoryList in Array.from(sortedCategories)"
|
|
||||||
:key="categoryList[0]"
|
|
||||||
class="categories"
|
|
||||||
>
|
|
||||||
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
|
|
||||||
<div v-for="category in categoryList[1]" :key="category.name">
|
|
||||||
<SearchFilter
|
|
||||||
:active-filters="facets"
|
|
||||||
:icon="category.icon"
|
|
||||||
:display-name="formatCategory(category.name)"
|
|
||||||
:facet-name="`categories:${encodeURIComponent(category.name)}`"
|
|
||||||
class="filter-checkbox"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="projectType !== 'datapack'" class="environment">
|
|
||||||
<h2>Environments</h2>
|
|
||||||
<SearchFilter
|
|
||||||
:active-filters="selectedEnvironments"
|
|
||||||
display-name="Client"
|
|
||||||
facet-name="client"
|
|
||||||
class="filter-checkbox"
|
|
||||||
>
|
|
||||||
<ClientIcon aria-hidden="true" />
|
|
||||||
</SearchFilter>
|
|
||||||
<SearchFilter
|
|
||||||
:active-filters="selectedEnvironments"
|
|
||||||
display-name="Server"
|
|
||||||
facet-name="server"
|
|
||||||
class="filter-checkbox"
|
|
||||||
>
|
|
||||||
<ServerIcon aria-hidden="true" />
|
|
||||||
</SearchFilter>
|
|
||||||
</div>
|
|
||||||
<div class="open-source">
|
|
||||||
<h2>Open source</h2>
|
|
||||||
<Checkbox v-model="onlyOpenSource" label="Open source only" class="filter-checkbox" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</aside>
|
|
||||||
<div ref="searchWrapper" class="search">
|
|
||||||
<Promotion class="promotion" :external="false" query-param="?r=launcher" />
|
|
||||||
<Card class="project-type-container">
|
|
||||||
<NavRow :links="selectableProjectTypes" />
|
|
||||||
</Card>
|
|
||||||
<Card class="search-panel-container" :class="{ highlighted: showSearch }">
|
|
||||||
<div class="iconified-input">
|
|
||||||
<SearchIcon aria-hidden="true" />
|
|
||||||
<input
|
|
||||||
v-model="query"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
:placeholder="`Search ${projectType}s...`"
|
|
||||||
/>
|
|
||||||
<Button @click="() => (query = '')">
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="inline-option">
|
|
||||||
<span>Sort by</span>
|
|
||||||
<DropdownSelect
|
|
||||||
v-model="sortType"
|
|
||||||
name="Sort by"
|
|
||||||
:options="sortTypes"
|
|
||||||
:display-name="(option) => option?.display"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="inline-option">
|
|
||||||
<span>Show per page</span>
|
|
||||||
<DropdownSelect
|
|
||||||
v-model="maxResults"
|
|
||||||
name="Max results"
|
|
||||||
:options="[5, 10, 15, 20, 50, 100]"
|
|
||||||
:default-value="maxResults"
|
|
||||||
:model-value="maxResults"
|
|
||||||
class="limit-dropdown"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Pagination :page="currentPage" :count="pageCount" class="pagination-before" />
|
|
||||||
<SplashScreen v-if="loading" />
|
|
||||||
<section v-else class="project-list display-mode--list instance-results" role="list">
|
|
||||||
<Card v-for="project in 20" :key="project" class="search-card button-base">
|
|
||||||
<div class="icon">
|
|
||||||
<Avatar
|
|
||||||
src="https://launcher-files.modrinth.com/assets/default_profile.png"
|
|
||||||
size="md"
|
|
||||||
class="search-icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="content-wrapper">
|
|
||||||
<div class="title joined-text">
|
|
||||||
<h2>Example Modpack</h2>
|
|
||||||
<span>by Modrinth</span>
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
A very cool project that does cool project things that you can your friends can do.
|
|
||||||
</div>
|
|
||||||
<div class="tags">
|
|
||||||
<Categories
|
|
||||||
:categories="
|
|
||||||
categories
|
|
||||||
.filter((cat) => cat.project_type === projectType)
|
|
||||||
.slice(project / 2, project / 2 + 3)
|
|
||||||
"
|
|
||||||
:type="modpack"
|
|
||||||
>
|
|
||||||
<EnvironmentIndicator
|
|
||||||
:type-only="true"
|
|
||||||
:client-side="true"
|
|
||||||
:server-side="true"
|
|
||||||
type="modpack"
|
|
||||||
:search="true"
|
|
||||||
/>
|
|
||||||
</Categories>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stats button-group">
|
|
||||||
<div v-if="featured" class="badge">
|
|
||||||
<StarIcon />
|
|
||||||
Featured
|
|
||||||
</div>
|
|
||||||
<div class="badge">
|
|
||||||
<DownloadIcon />
|
|
||||||
{{ formatNumber(420) }}
|
|
||||||
</div>
|
|
||||||
<div class="badge">
|
|
||||||
<HeartIcon />
|
|
||||||
{{ formatNumber(69) }}
|
|
||||||
</div>
|
|
||||||
<div class="badge">
|
|
||||||
<CalendarIcon />
|
|
||||||
A minute ago
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
<pagination :page="currentPage" :count="pageCount" class="pagination-after" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
|
||||||
<style lang="scss">
|
|
||||||
.small-instance {
|
|
||||||
min-height: unset !important;
|
|
||||||
|
|
||||||
.instance {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-instance_info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-checkbox {
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
display: flex;
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.checkbox {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.project-type-dropdown {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.promotion {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-type-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-panel-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
min-height: min-content !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
input {
|
|
||||||
max-width: none !important;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-panel-container {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
width: 100%;
|
|
||||||
padding: 1rem !important;
|
|
||||||
white-space: nowrap;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
.inline-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
.sort-dropdown {
|
|
||||||
max-width: 12.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.limit-dropdown {
|
|
||||||
width: 5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-panel {
|
|
||||||
button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
margin-right: 0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.filter-panel {
|
|
||||||
position: fixed;
|
|
||||||
width: 20rem;
|
|
||||||
padding: 1rem 0.5rem 1rem 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: fit-content;
|
|
||||||
min-height: calc(100vh - 3.25rem);
|
|
||||||
max-height: calc(100vh - 3.25rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 1.16rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
margin: 0 1rem 0.5rem 20.5rem;
|
|
||||||
width: calc(100% - 20.5rem);
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
margin: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-card {
|
|
||||||
margin-bottom: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 6rem auto 7rem;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
&:active:not(&:disabled) {
|
|
||||||
scale: 0.98 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.joined-text {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
column-gap: 0.5rem;
|
|
||||||
align-items: baseline;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
align-self: center;
|
|
||||||
height: 6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-wrapper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
grid-column: 2 / 4;
|
|
||||||
flex-direction: column;
|
|
||||||
grid-row: 1;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
.description {
|
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats {
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
grid-row: 2;
|
|
||||||
justify-self: stretch;
|
|
||||||
align-self: start;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,270 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { Card, Slider, DropdownSelect, Toggle } from 'omorphia'
|
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|
||||||
|
|
||||||
const pageOptions = ['Home', 'Library']
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="settings-page">
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Display</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="theme">
|
|
||||||
<span class="label__title">Color theme</span>
|
|
||||||
<span class="label__description">Change the global launcher color theme.</span>
|
|
||||||
</label>
|
|
||||||
<DropdownSelect
|
|
||||||
id="theme"
|
|
||||||
name="Theme dropdown"
|
|
||||||
:options="['Dark']"
|
|
||||||
:disabled="true"
|
|
||||||
:default-value="'dark'"
|
|
||||||
class="theme-dropdown disable-children"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="collapsed-nav">
|
|
||||||
<span class="label__title">Collapsed navigation mode</span>
|
|
||||||
<span class="label__description"
|
|
||||||
>Change the style of the side navigation bar to a compact version.</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<Toggle id="collapsed-nav" :checked="false" :disabled="true" />
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="advanced-rendering">
|
|
||||||
<span class="label__title">Advanced rendering</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Enables advanced rendering such as blur effects that may cause performance issues
|
|
||||||
without hardware-accelerated rendering.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle id="advanced-rendering" :checked="true" :disabled="true" />
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="minimize-launcher">
|
|
||||||
<span class="label__title">Minimize launcher</span>
|
|
||||||
<span class="label__description"
|
|
||||||
>Minimize the launcher when a Minecraft process starts.</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<Toggle id="minimize-launcher" :checked="false" :disabled="true" />
|
|
||||||
</div>
|
|
||||||
<div class="opening-page">
|
|
||||||
<label for="opening-page">
|
|
||||||
<span class="label__title">Default landing page</span>
|
|
||||||
<span class="label__description">Change the page to which the launcher opens on.</span>
|
|
||||||
</label>
|
|
||||||
<DropdownSelect
|
|
||||||
id="opening-page"
|
|
||||||
name="Opening page dropdown"
|
|
||||||
:options="pageOptions"
|
|
||||||
default-value="Home"
|
|
||||||
class="opening-page"
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Resource management</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="max-downloads">
|
|
||||||
<span class="label__title">Maximum concurrent downloads</span>
|
|
||||||
<span class="label__description"
|
|
||||||
>The maximum amount of files the launcher can download at the same time. Set this to a
|
|
||||||
lower value if you have a poor internet connection.</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<Slider id="max-downloads" :min="1" :max="10" :step="1" :disabled="true" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="max-writes">
|
|
||||||
<span class="label__title">Maximum concurrent writes</span>
|
|
||||||
<span class="label__description"
|
|
||||||
>The maximum amount of files the launcher can write to the disk at once. Set this to a
|
|
||||||
lower value if you are frequently getting I/O errors.</span
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<Slider id="max-writes" :min="1" :max="50" :step="1" :disabled="true" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Privacy</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="opt-out-analytics">
|
|
||||||
<span class="label__title">Disable analytics</span>
|
|
||||||
<span class="label__description">
|
|
||||||
Modrinth collects anonymized analytics and usage data to improve our user experience and
|
|
||||||
customize your experience. Opting out will disable this data collection.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Toggle id="opt-out-analytics" :disabled="true" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Java settings</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<label for="java-17">
|
|
||||||
<span class="label__title">Java 17 location</span>
|
|
||||||
</label>
|
|
||||||
<JavaSelector id="java-17" :version="17" model-value="" :disabled="true" />
|
|
||||||
<label for="java-8">
|
|
||||||
<span class="label__title">Java 8 location</span>
|
|
||||||
</label>
|
|
||||||
<JavaSelector id="java-8" :version="8" model-value="" :disabled="true" />
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<label for="java-args">
|
|
||||||
<span class="label__title">Java arguments</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="java-args"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
class="installation-input"
|
|
||||||
placeholder="Enter java arguments..."
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<label for="env-vars">
|
|
||||||
<span class="label__title">Environmental variables</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="env-vars"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
class="installation-input"
|
|
||||||
placeholder="Enter environmental variables..."
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
<hr class="card-divider" />
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="max-memory">
|
|
||||||
<span class="label__title">Java memory</span>
|
|
||||||
<span class="label__description">
|
|
||||||
The memory allocated to each instance when it is ran.
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Slider id="max-memory" :min="256" :max="10256" :step="1" unit="mb" :disabled="true" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Hooks</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="pre-launch">
|
|
||||||
<span class="label__title">Pre launch</span>
|
|
||||||
<span class="label__description"> Ran before the instance is launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="pre-launch"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter pre-launch command..."
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="wrapper">
|
|
||||||
<span class="label__title">Wrapper</span>
|
|
||||||
<span class="label__description"> Wrapper command for launching Minecraft. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="wrapper"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter wrapper command..."
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="post-exit">
|
|
||||||
<span class="label__title">Post exit</span>
|
|
||||||
<span class="label__description"> Ran after the game closes. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="post-exit"
|
|
||||||
autocomplete="off"
|
|
||||||
type="text"
|
|
||||||
placeholder="Enter post-exit command..."
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<div class="label">
|
|
||||||
<h3>
|
|
||||||
<span class="label__title size-card-header">Window size</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="width">
|
|
||||||
<span class="label__title">Width</span>
|
|
||||||
<span class="label__description"> The width of the game window when launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="width"
|
|
||||||
autocomplete="off"
|
|
||||||
type="number"
|
|
||||||
placeholder="Enter width..."
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="adjacent-input">
|
|
||||||
<label for="height">
|
|
||||||
<span class="label__title">Height</span>
|
|
||||||
<span class="label__description"> The height of the game window when launched. </span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="height"
|
|
||||||
autocomplete="off"
|
|
||||||
type="number"
|
|
||||||
class="input"
|
|
||||||
placeholder="Enter height..."
|
|
||||||
:disabled="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.settings-page {
|
|
||||||
margin: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.installation-input {
|
|
||||||
width: 100% !important;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-dropdown {
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-divider {
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disable-children * {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,293 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Checkbox,
|
|
||||||
Chips,
|
|
||||||
XIcon,
|
|
||||||
FolderOpenIcon,
|
|
||||||
FolderSearchIcon,
|
|
||||||
UpdatedIcon,
|
|
||||||
} from 'omorphia'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import {
|
|
||||||
get_default_launcher_path,
|
|
||||||
get_importable_instances,
|
|
||||||
import_instance,
|
|
||||||
} from '@/helpers/import.js'
|
|
||||||
import { open } from '@tauri-apps/api/dialog'
|
|
||||||
import { handleError } from '@/store/state.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
nextPage: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
prevPage: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const profiles = ref(
|
|
||||||
new Map([
|
|
||||||
['MultiMC', []],
|
|
||||||
['GDLauncher', []],
|
|
||||||
['ATLauncher', []],
|
|
||||||
['Curseforge', []],
|
|
||||||
['PrismLauncher', []],
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const selectedProfileType = ref('MultiMC')
|
|
||||||
const profileOptions = ref([
|
|
||||||
{ name: 'MultiMC', path: '' },
|
|
||||||
{ name: 'GDLauncher', path: '' },
|
|
||||||
{ name: 'ATLauncher', path: '' },
|
|
||||||
{ name: 'Curseforge', path: '' },
|
|
||||||
{ name: 'PrismLauncher', path: '' },
|
|
||||||
])
|
|
||||||
|
|
||||||
// Attempt to get import profiles on default paths
|
|
||||||
const promises = profileOptions.value.map(async (option) => {
|
|
||||||
const path = await get_default_launcher_path(option.name).catch(handleError)
|
|
||||||
if (!path || path === '') return
|
|
||||||
|
|
||||||
// Try catch to allow failure and simply ignore default path attempt
|
|
||||||
try {
|
|
||||||
const instances = await get_importable_instances(option.name, path)
|
|
||||||
|
|
||||||
if (!instances) return
|
|
||||||
profileOptions.value.find((profile) => profile.name === option.name).path = path
|
|
||||||
profiles.value.set(
|
|
||||||
option.name,
|
|
||||||
instances.map((name) => ({ name, selected: false })),
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
// Allow failure silently
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Promise.all(promises)
|
|
||||||
|
|
||||||
const selectLauncherPath = async () => {
|
|
||||||
selectedProfileType.value.path = await open({ multiple: false, directory: true })
|
|
||||||
|
|
||||||
if (selectedProfileType.value.path) {
|
|
||||||
await reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = async () => {
|
|
||||||
const instances = await get_importable_instances(
|
|
||||||
selectedProfileType.value.name,
|
|
||||||
selectedProfileType.value.path,
|
|
||||||
).catch(handleError)
|
|
||||||
profiles.value.set(
|
|
||||||
selectedProfileType.value.name,
|
|
||||||
instances.map((name) => ({ name, selected: false })),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const setPath = () => {
|
|
||||||
profileOptions.value.find((profile) => profile.name === selectedProfileType.value.name).path =
|
|
||||||
selectedProfileType.value.path
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = async () => {
|
|
||||||
loading.value = true
|
|
||||||
for (const launcher of Array.from(profiles.value.entries()).map(([launcher, profiles]) => ({
|
|
||||||
launcher,
|
|
||||||
path: profileOptions.value.find((option) => option.name === launcher).path,
|
|
||||||
profiles,
|
|
||||||
}))) {
|
|
||||||
for (const profile of launcher.profiles.filter((profile) => profile.selected)) {
|
|
||||||
await import_instance(launcher.launcher, launcher.path, profile.name)
|
|
||||||
.catch(handleError)
|
|
||||||
.then(() => console.log(`Successfully Imported ${profile.name} from ${launcher.launcher}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loading.value = false
|
|
||||||
props.nextPage()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card>
|
|
||||||
<h2>Importing external profiles</h2>
|
|
||||||
<Chips
|
|
||||||
v-model="selectedProfileType"
|
|
||||||
:items="profileOptions"
|
|
||||||
:format-label="(profile) => profile?.name"
|
|
||||||
/>
|
|
||||||
<div class="path-selection">
|
|
||||||
<h3>{{ selectedProfileType.name }} path</h3>
|
|
||||||
<div class="path-input">
|
|
||||||
<div class="iconified-input">
|
|
||||||
<FolderOpenIcon />
|
|
||||||
<input
|
|
||||||
v-model="selectedProfileType.path"
|
|
||||||
type="text"
|
|
||||||
placeholder="Path to launcher"
|
|
||||||
@change="setPath"
|
|
||||||
/>
|
|
||||||
<Button @click="() => (selectedLauncherPath = '')">
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Button icon-only @click="selectLauncherPath">
|
|
||||||
<FolderSearchIcon />
|
|
||||||
</Button>
|
|
||||||
<Button icon-only @click="reload">
|
|
||||||
<UpdatedIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="table">
|
|
||||||
<div class="table-head table-row">
|
|
||||||
<div class="toggle-all table-cell">
|
|
||||||
<Checkbox
|
|
||||||
class="select-checkbox"
|
|
||||||
:model-value="profiles.get(selectedProfileType.name)?.every((child) => child.selected)"
|
|
||||||
@update:model-value="
|
|
||||||
(newValue) =>
|
|
||||||
profiles
|
|
||||||
.get(selectedProfileType.name)
|
|
||||||
?.forEach((child) => (child.selected = newValue))
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="name-cell table-cell">Profile name</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
profiles.get(selectedProfileType.name) &&
|
|
||||||
profiles.get(selectedProfileType.name).length > 0
|
|
||||||
"
|
|
||||||
class="table-content"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(profile, index) in profiles.get(selectedProfileType.name)"
|
|
||||||
:key="index"
|
|
||||||
class="table-row"
|
|
||||||
>
|
|
||||||
<div class="checkbox-cell table-cell">
|
|
||||||
<Checkbox v-model="profile.selected" class="select-checkbox" />
|
|
||||||
</div>
|
|
||||||
<div class="name-cell table-cell">
|
|
||||||
{{ profile.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="table-content empty">No profiles found</div>
|
|
||||||
</div>
|
|
||||||
<div class="button-row">
|
|
||||||
<Button class="transparent" @click="prevPage"> Back </Button>
|
|
||||||
<Button
|
|
||||||
:disabled="
|
|
||||||
loading ||
|
|
||||||
!Array.from(profiles.values())
|
|
||||||
.flatMap((e) => e)
|
|
||||||
.some((e) => e.selected)
|
|
||||||
"
|
|
||||||
color="primary"
|
|
||||||
@click="next"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
loading
|
|
||||||
? 'Importing...'
|
|
||||||
: Array.from(profiles.values())
|
|
||||||
.flatMap((e) => e)
|
|
||||||
.some((e) => e.selected)
|
|
||||||
? `Import ${
|
|
||||||
Array.from(profiles.values())
|
|
||||||
.flatMap((e) => e)
|
|
||||||
.filter((e) => e.selected).length
|
|
||||||
} profiles`
|
|
||||||
: 'Select profiles to import'
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
<Button class="transparent" @click="nextPage"> Next </Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.card {
|
|
||||||
padding: var(--gap-xl);
|
|
||||||
min-height: unset;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-selection {
|
|
||||||
padding: var(--gap-xl);
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: var(--gap-md);
|
|
||||||
gap: var(--gap-md);
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.path-input {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
|
|
||||||
.iconified-input {
|
|
||||||
flex-grow: 1;
|
|
||||||
:deep(input) {
|
|
||||||
width: 100%;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
border: 1px solid var(--color-bg);
|
|
||||||
margin-bottom: var(--gap-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row {
|
|
||||||
grid-template-columns: min-content auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-content {
|
|
||||||
max-height: calc(5 * (18px + 2rem));
|
|
||||||
height: calc(5 * (18px + 2rem));
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-checkbox {
|
|
||||||
button.checkbox {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
padding: var(--gap-sm) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bolder;
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,18 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button, LogInIcon, Modal, ClipboardCopyIcon, GlobeIcon, Card } from 'omorphia'
|
import { Button, LogInIcon, Card } from 'omorphia'
|
||||||
import { authenticate_await_completion, authenticate_begin_flow } from '@/helpers/auth.js'
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { useTheming } from '@/store/theme.js'
|
|
||||||
import mixpanel from 'mixpanel-browser'
|
import mixpanel from 'mixpanel-browser'
|
||||||
import { get, set } from '@/helpers/settings.js'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import QrcodeVue from 'qrcode.vue'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
const loading = ref(false)
|
||||||
const themeStore = useTheming()
|
|
||||||
const loginUrl = ref(null)
|
|
||||||
const loginModal = ref()
|
|
||||||
const loginCode = ref(null)
|
|
||||||
const finalizedLogin = ref(false)
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
nextPage: {
|
nextPage: {
|
||||||
@ -26,42 +19,21 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
const loginSuccess = await authenticate_begin_flow().catch(handleError)
|
try {
|
||||||
loginUrl.value = loginSuccess.verification_uri
|
loading.value = true
|
||||||
loginCode.value = loginSuccess.user_code
|
const loggedIn = await login_flow()
|
||||||
loginModal.value.show()
|
|
||||||
|
|
||||||
await window.__TAURI_INVOKE__('tauri', {
|
if (loggedIn) {
|
||||||
__tauriModule: 'Shell',
|
await set_default_user(loggedIn.id).catch(handleError)
|
||||||
message: {
|
}
|
||||||
cmd: 'open',
|
|
||||||
path: loginSuccess.verification_uri,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const loggedIn = await authenticate_await_completion().catch(handleError)
|
|
||||||
loginModal.value.hide()
|
|
||||||
|
|
||||||
const settings = await get().catch(handleError)
|
|
||||||
settings.default_user = loggedIn.id
|
|
||||||
await set(settings).catch(handleError)
|
|
||||||
finalizedLogin.value = true
|
|
||||||
await mixpanel.track('AccountLogIn')
|
await mixpanel.track('AccountLogIn')
|
||||||
|
loading.value = false
|
||||||
props.nextPage()
|
props.nextPage()
|
||||||
}
|
} catch (err) {
|
||||||
|
loading.value = false
|
||||||
const openUrl = async () => {
|
handleSevereError(err)
|
||||||
await window.__TAURI_INVOKE__('tauri', {
|
}
|
||||||
__tauriModule: 'Shell',
|
|
||||||
message: {
|
|
||||||
cmd: 'open',
|
|
||||||
path: loginUrl.value,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clipboardWrite = async (a) => {
|
|
||||||
navigator.clipboard.writeText(a)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -87,45 +59,15 @@ const clipboardWrite = async (a) => {
|
|||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<Button class="transparent" large @click="prevPage"> Back </Button>
|
<Button class="transparent" large @click="prevPage"> Back </Button>
|
||||||
<div class="sign-in-pair">
|
<div class="sign-in-pair">
|
||||||
<Button color="primary" large @click="login">
|
<Button color="primary" large :disabled="loading" @click="login">
|
||||||
<LogInIcon v-if="!finalizedLogin" />
|
<LogInIcon />
|
||||||
{{ finalizedLogin ? 'Next' : 'Sign in' }}
|
{{ loading ? 'Loading...' : 'Sign in' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button class="transparent" large @click="nextPage()"> Next </Button>
|
<Button class="transparent" large @click="nextPage()"> Finish</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Modal ref="loginModal" header="Signing in" :noblur="!themeStore.advancedRendering">
|
|
||||||
<div class="modal-body">
|
|
||||||
<QrcodeVue :value="loginUrl" class="qr-code" margin="3" size="160" />
|
|
||||||
<div class="modal-text">
|
|
||||||
<div class="label">Copy this code</div>
|
|
||||||
<div class="code-text">
|
|
||||||
<div class="code">
|
|
||||||
{{ loginCode }}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
v-tooltip="'Copy code'"
|
|
||||||
icon-only
|
|
||||||
large
|
|
||||||
color="raised"
|
|
||||||
@click="() => clipboardWrite(loginCode)"
|
|
||||||
>
|
|
||||||
<ClipboardCopyIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>And enter it on Microsoft's website to sign in.</div>
|
|
||||||
<div class="iconified-input">
|
|
||||||
<LogInIcon />
|
|
||||||
<input type="text" :value="loginUrl" readonly />
|
|
||||||
<Button v-tooltip="'Open link'" icon-only color="raised" @click="openUrl">
|
|
||||||
<GlobeIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -188,79 +130,10 @@ const clipboardWrite = async (a) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-code {
|
|
||||||
background-color: white !important;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--gap-lg);
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--gap-lg);
|
|
||||||
|
|
||||||
.modal-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
h2,
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: var(--gap-xs);
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.code {
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: solid 1px var(--color-button-bg);
|
|
||||||
font-family: var(--mono-font);
|
|
||||||
letter-spacing: var(--gap-md);
|
|
||||||
color: var(--color-contrast);
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: var(--gap-sm) 0 var(--gap-sm) var(--gap-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticker {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 25rem;
|
|
||||||
height: auto;
|
|
||||||
margin-bottom: var(--gap-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sign-in-pair {
|
.sign-in-pair {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-sm);
|
gap: var(--gap-sm);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.code {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--color-base);
|
|
||||||
color: var(--color-contrast);
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,28 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { Button } from 'omorphia'
|
||||||
Button,
|
|
||||||
HomeIcon,
|
|
||||||
SearchIcon,
|
|
||||||
LibraryIcon,
|
|
||||||
PlusIcon,
|
|
||||||
SettingsIcon,
|
|
||||||
XIcon,
|
|
||||||
Notifications,
|
|
||||||
} from 'omorphia'
|
|
||||||
import { appWindow } from '@tauri-apps/api/window'
|
|
||||||
import { saveWindowState, StateFlags } from 'tauri-plugin-window-state-api'
|
|
||||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
|
||||||
import FakeAppBar from '@/components/ui/tutorial/FakeAppBar.vue'
|
|
||||||
import FakeAccountsCard from '@/components/ui/tutorial/FakeAccountsCard.vue'
|
|
||||||
import { MinimizeIcon, MaximizeIcon } from '@/assets/icons'
|
|
||||||
import ModrinthLoadingIndicator from '@/components/modrinth-loading-indicator.js'
|
|
||||||
import FakeSearch from '@/components/ui/tutorial/FakeSearch.vue'
|
|
||||||
import FakeGridDisplay from '@/components/ui/tutorial/FakeGridDisplay.vue'
|
|
||||||
import FakeRowDisplay from '@/components/ui/tutorial/FakeRowDisplay.vue'
|
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { window } from '@tauri-apps/api'
|
|
||||||
import TutorialTip from '@/components/ui/tutorial/TutorialTip.vue'
|
|
||||||
import FakeSettings from '@/components/ui/tutorial/FakeSettings.vue'
|
|
||||||
import { get, set } from '@/helpers/settings.js'
|
import { get, set } from '@/helpers/settings.js'
|
||||||
import mixpanel from 'mixpanel-browser'
|
import mixpanel from 'mixpanel-browser'
|
||||||
import GalleryImage from '@/components/ui/tutorial/GalleryImage.vue'
|
import GalleryImage from '@/components/ui/tutorial/GalleryImage.vue'
|
||||||
@ -30,11 +8,7 @@ import LoginCard from '@/components/ui/tutorial/LoginCard.vue'
|
|||||||
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
|
import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
|
||||||
import { auto_install_java, get_jre } from '@/helpers/jre.js'
|
import { auto_install_java, get_jre } from '@/helpers/jre.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import ImportingCard from '@/components/ui/tutorial/ImportingCard.vue'
|
|
||||||
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
|
||||||
import PreImportScreen from '@/components/ui/tutorial/PreImportScreen.vue'
|
|
||||||
|
|
||||||
const phase = ref(0)
|
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -46,15 +20,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const flow = ref('')
|
const flow = ref('')
|
||||||
|
|
||||||
const nextPhase = () => {
|
|
||||||
phase.value++
|
|
||||||
mixpanel.track('TutorialPhase', { page: phase.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevPhase = () => {
|
|
||||||
phase.value--
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextPage = (newFlow) => {
|
const nextPage = (newFlow) => {
|
||||||
page.value++
|
page.value++
|
||||||
mixpanel.track('OnboardingPage', { page: page.value })
|
mixpanel.track('OnboardingPage', { page: page.value })
|
||||||
@ -64,10 +29,6 @@ const nextPage = (newFlow) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const endOnboarding = () => {
|
|
||||||
nextPhase()
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevPage = () => {
|
const prevPage = () => {
|
||||||
page.value--
|
page.value--
|
||||||
}
|
}
|
||||||
@ -105,18 +66,18 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="phase === 0" class="onboarding">
|
<div class="onboarding">
|
||||||
<StickyTitleBar />
|
<StickyTitleBar />
|
||||||
<GalleryImage
|
<GalleryImage
|
||||||
v-if="page === 1"
|
v-if="page === 1"
|
||||||
:gallery="[
|
:gallery="[
|
||||||
{
|
{
|
||||||
url: 'https://cdn.discordapp.com/attachments/817413688771608587/1131109353928265809/Screenshot_2023-07-15_at_4.16.18_PM.png',
|
url: 'https://launcher-files.modrinth.com/onboarding/home.png',
|
||||||
title: 'Discovery',
|
title: 'Discovery',
|
||||||
subtitle: 'See the latest and greatest mods and modpacks to play with from Modrinth',
|
subtitle: 'See the latest and greatest mods and modpacks to play with from Modrinth',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: 'https://cdn.discordapp.com/attachments/817413688771608587/1131109354238640238/Screenshot_2023-07-15_at_4.17.43_PM.png',
|
url: 'https://launcher-files.modrinth.com/onboarding/discover.png',
|
||||||
title: 'Profile Management',
|
title: 'Profile Management',
|
||||||
subtitle:
|
subtitle:
|
||||||
'Play, manage and search through all the amazing profiles downloaded on your computer at any time, even offline!',
|
'Play, manage and search through all the amazing profiles downloaded on your computer at any time, even offline!',
|
||||||
@ -126,185 +87,7 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<Button color="primary" @click="nextPage"> Get started </Button>
|
<Button color="primary" @click="nextPage"> Get started </Button>
|
||||||
</GalleryImage>
|
</GalleryImage>
|
||||||
<LoginCard v-else-if="page === 2" :next-page="nextPage" :prev-page="prevPage" />
|
<LoginCard v-else-if="page === 2" :next-page="finishOnboarding" :prev-page="prevPage" />
|
||||||
<ModrinthLoginScreen
|
|
||||||
v-else-if="page === 3"
|
|
||||||
:modal="false"
|
|
||||||
:next-page="nextPage"
|
|
||||||
:prev-page="prevPage"
|
|
||||||
:flow="flow"
|
|
||||||
/>
|
|
||||||
<PreImportScreen
|
|
||||||
v-else-if="page === 4"
|
|
||||||
:next-page="endOnboarding"
|
|
||||||
:prev-page="prevPage"
|
|
||||||
:import-page="nextPage"
|
|
||||||
/>
|
|
||||||
<ImportingCard v-else-if="page === 5" :next-page="endOnboarding" :prev-page="prevPage" />
|
|
||||||
</div>
|
|
||||||
<div v-else class="container">
|
|
||||||
<StickyTitleBar v-if="phase === 9" />
|
|
||||||
<div v-if="phase < 9" class="nav-container">
|
|
||||||
<div class="nav-section">
|
|
||||||
<FakeAccountsCard :show-demo="phase === 3">
|
|
||||||
<TutorialTip
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:previous-function="prevPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Signing in"
|
|
||||||
description="The Modrinth App uses your Microsoft account to allow you to launch Minecraft. You can sign in with your Microsoft account here, and switch between multiple accounts."
|
|
||||||
/>
|
|
||||||
</FakeAccountsCard>
|
|
||||||
<div class="pages-list">
|
|
||||||
<div class="btn icon-only" :class="{ active: phase < 4 }">
|
|
||||||
<HomeIcon />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="btn icon-only"
|
|
||||||
:class="{ active: phase === 4 || phase === 5, highlighted: phase === 4 }"
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="btn icon-only"
|
|
||||||
:class="{
|
|
||||||
active: phase === 6 || phase === 7,
|
|
||||||
highlighted: phase === 6,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<LibraryIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="settings pages-list">
|
|
||||||
<Button class="sleek-primary" icon-only>
|
|
||||||
<PlusIcon />
|
|
||||||
</Button>
|
|
||||||
<Button icon-only :class="{ active: phase === 8, highlighted: phase === 8 }">
|
|
||||||
<SettingsIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="phase < 9" class="view">
|
|
||||||
<div data-tauri-drag-region class="appbar">
|
|
||||||
<section class="navigation-controls">
|
|
||||||
<Breadcrumbs data-tauri-drag-region />
|
|
||||||
</section>
|
|
||||||
<section class="mod-stats">
|
|
||||||
<FakeAppBar
|
|
||||||
:show-running="phase === 7"
|
|
||||||
:show-download="phase === 5"
|
|
||||||
:exit="finishOnboarding"
|
|
||||||
>
|
|
||||||
<template #running>
|
|
||||||
<TutorialTip
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:previous-function="prevPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Playing modpacks"
|
|
||||||
description="When you launch a modpack, you can manage it directly in the title bar here. You can stop the modpack, view the logs, and see all currently running packs."
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #download>
|
|
||||||
<TutorialTip
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:previous-function="prevPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Installing modpacks"
|
|
||||||
description="When you download a modpack, Modrinth App will automatically install it for you. You can view the progress of the installation here."
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</FakeAppBar>
|
|
||||||
</section>
|
|
||||||
<section class="window-controls">
|
|
||||||
<Button class="titlebar-button" icon-only @click="() => appWindow.minimize()">
|
|
||||||
<MinimizeIcon />
|
|
||||||
</Button>
|
|
||||||
<Button class="titlebar-button" icon-only @click="() => appWindow.toggleMaximize()">
|
|
||||||
<MaximizeIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
class="titlebar-button close"
|
|
||||||
icon-only
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
saveWindowState(StateFlags.ALL)
|
|
||||||
window.getCurrent().close()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
<div class="router-view">
|
|
||||||
<ModrinthLoadingIndicator
|
|
||||||
offset-height="var(--appbar-height)"
|
|
||||||
offset-width="var(--sidebar-width)"
|
|
||||||
/>
|
|
||||||
<Notifications ref="notificationsWrapper" />
|
|
||||||
<FakeRowDisplay v-if="phase < 4 || phase > 8" :show-instance="phase === 2" />
|
|
||||||
<FakeGridDisplay v-if="phase === 6 || phase === 7" :show-instances="phase === 6" />
|
|
||||||
<suspense>
|
|
||||||
<FakeSearch v-if="phase === 4 || phase === 5" :show-search="phase === 4" />
|
|
||||||
</suspense>
|
|
||||||
<FakeSettings v-if="phase === 8" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TutorialTip
|
|
||||||
v-if="phase === 1"
|
|
||||||
class="first-tip highlighted"
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Enter the Modrinth App!"
|
|
||||||
description="This is the Modrinth App guide. Key parts are marked with a green shadow. Click 'Next' to
|
|
||||||
proceed. You can leave the tutorial anytime using the Exit button above the plus button on the bottom left."
|
|
||||||
/>
|
|
||||||
<div v-if="phase === 1" class="whole-page-shadow" />
|
|
||||||
<TutorialTip
|
|
||||||
v-if="phase === 2"
|
|
||||||
class="sticky-tip"
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:previous-function="prevPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Home page"
|
|
||||||
description="This is the home page. Here you can see all the latest modpacks, mods, and other content on Modrinth. You can also see a few of your installed modpacks here."
|
|
||||||
/>
|
|
||||||
<TutorialTip
|
|
||||||
v-if="phase === 4"
|
|
||||||
class="sticky-tip"
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:previous-function="prevPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Searching for content"
|
|
||||||
description="You can search for content on Modrinth by navigating to the search page. You can search for mods, modpacks, and more, and install them directly from here."
|
|
||||||
/>
|
|
||||||
<TutorialTip
|
|
||||||
v-if="phase === 6"
|
|
||||||
class="sticky-tip"
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:previous-function="prevPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Modpack library"
|
|
||||||
description="You can view all your installed modpacks in the library. You can launch any modpack from here, or click the card to view more information about it."
|
|
||||||
/>
|
|
||||||
<TutorialTip
|
|
||||||
v-if="phase === 8"
|
|
||||||
class="sticky-tip"
|
|
||||||
:progress-function="nextPhase"
|
|
||||||
:previous-function="prevPhase"
|
|
||||||
:progress="phase"
|
|
||||||
title="Settings"
|
|
||||||
description="You will be able to view and change the settings for the Modrinth App here. You can change the appearance, set and download new Java versions, and more."
|
|
||||||
/>
|
|
||||||
<TutorialTip
|
|
||||||
v-if="phase === 9"
|
|
||||||
class="final-tip highlighted"
|
|
||||||
:progress-function="finishOnboarding"
|
|
||||||
:progress="phase"
|
|
||||||
title="Enter the Modrinth App!"
|
|
||||||
description="That's it! You're ready to use the Modrinth App. If you need help, you can always join our discord server!"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -1,184 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { Button, Card, ModrinthIcon } from 'omorphia'
|
|
||||||
import { ATLauncherIcon, PrismIcon } from '@/assets/external/index.js'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
nextPage: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
prevPage: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
importPage: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card class="import-card">
|
|
||||||
<div class="base-ellipsis ellipsis-1" />
|
|
||||||
<div class="base-ellipsis ellipsis-2" />
|
|
||||||
<div class="base-ellipsis ellipsis-3" />
|
|
||||||
<div class="base-ellipsis ellipsis-4" />
|
|
||||||
<div class="logo">
|
|
||||||
<ModrinthIcon />
|
|
||||||
</div>
|
|
||||||
<div class="launcher-stamp top-left">
|
|
||||||
<ATLauncherIcon />
|
|
||||||
</div>
|
|
||||||
<div class="launcher-stamp top-right">
|
|
||||||
<PrismIcon />
|
|
||||||
</div>
|
|
||||||
<div class="launcher-stamp bottom-left">
|
|
||||||
<img src="@/assets/external/gdlauncher.png" alt="GDLauncher" />
|
|
||||||
</div>
|
|
||||||
<div class="launcher-stamp bottom-right">
|
|
||||||
<img src="@/assets/external/multimc.webp" alt="MultiMC" />
|
|
||||||
</div>
|
|
||||||
<div class="info-section">
|
|
||||||
<h2>Importing</h2>
|
|
||||||
<div class="markdown-body">
|
|
||||||
<p>
|
|
||||||
You can import projects from other launchers by clicking below, or you can skip ahead.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="button-row">
|
|
||||||
<Button class="transparent" @click="prevPage"> Back </Button>
|
|
||||||
<Button color="primary" @click="importPage"> Import </Button>
|
|
||||||
<Button class="transparent" @click="nextPage"> Next </Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.import-card {
|
|
||||||
width: 40rem;
|
|
||||||
height: 32rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.base-ellipsis {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
border-radius: 100%;
|
|
||||||
top: calc(var(--gap-xl) + 5rem);
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 100%;
|
|
||||||
background-color: rgba(#1bd96a, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis-1 {
|
|
||||||
width: 15rem;
|
|
||||||
height: 15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis-2 {
|
|
||||||
width: 30rem;
|
|
||||||
height: 30rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis-3 {
|
|
||||||
width: 45rem;
|
|
||||||
height: 45rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--gap-xl) + 5rem);
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--color-accent-contrast);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
|
||||||
z-index: 1;
|
|
||||||
width: 7rem;
|
|
||||||
height: 7rem;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.launcher-stamp {
|
|
||||||
position: absolute;
|
|
||||||
width: 5rem;
|
|
||||||
height: 5rem;
|
|
||||||
background-color: var(--color-accent-contrast);
|
|
||||||
border-radius: 50%;
|
|
||||||
z-index: 1;
|
|
||||||
opacity: 0.65;
|
|
||||||
padding: var(--gap-lg);
|
|
||||||
|
|
||||||
&.top-left {
|
|
||||||
top: var(--gap-xl);
|
|
||||||
left: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top-right {
|
|
||||||
top: var(--gap-xl);
|
|
||||||
right: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom-left {
|
|
||||||
top: 12rem;
|
|
||||||
left: 5.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom-right {
|
|
||||||
top: 12rem;
|
|
||||||
right: 5.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg,
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section {
|
|
||||||
position: absolute;
|
|
||||||
bottom: var(--gap-xl);
|
|
||||||
left: 50%;
|
|
||||||
width: 30rem;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
padding: var(--gap-xl);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
backdrop-filter: blur(1rem) brightness(0.4);
|
|
||||||
-webkit-backdrop-filter: blur(1rem) brightness(0.4);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
width: 100%;
|
|
||||||
align-content: center;
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
padding: var(--gap-sm) 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
|
||||||
import { Button, Card } from 'omorphia'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
progress: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: 'Tutorial',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: 'This is a tutorial',
|
|
||||||
},
|
|
||||||
progressFunction: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
previousFunction: {
|
|
||||||
type: Function,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card class="tutorial-card">
|
|
||||||
<h3 class="tutorial-title">
|
|
||||||
{{ title }}
|
|
||||||
</h3>
|
|
||||||
<div class="tutorial-body">
|
|
||||||
{{ description }}
|
|
||||||
</div>
|
|
||||||
<div class="tutorial-footer">
|
|
||||||
<Button v-if="previousFunction" class="transparent" @click="previousFunction"> Back </Button>
|
|
||||||
{{ progress }}/9
|
|
||||||
<ProgressBar :progress="(progress / 9) * 100" />
|
|
||||||
<Button color="primary" :action="progressFunction">
|
|
||||||
{{ progress === 9 ? 'Finish' : 'Next' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.tutorial-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--gap-md);
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
width: 22rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-title {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tutorial-footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--gap-sm);
|
|
||||||
|
|
||||||
.transparent {
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -18,28 +18,20 @@ import { invoke } from '@tauri-apps/api/tauri'
|
|||||||
/// This returns a DeviceLoginSuccess object, with two relevant fields:
|
/// This returns a DeviceLoginSuccess object, with two relevant fields:
|
||||||
/// - verification_uri: the URL to go to to complete the flow
|
/// - verification_uri: the URL to go to to complete the flow
|
||||||
/// - user_code: the code to enter on the verification_uri page
|
/// - user_code: the code to enter on the verification_uri page
|
||||||
export async function authenticate_begin_flow() {
|
export async function login() {
|
||||||
return await invoke('plugin:auth|auth_authenticate_begin_flow')
|
return await invoke('auth_login')
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate a user with Hydra - part 2
|
/// Retrieves the default user
|
||||||
/// This completes the authentication flow quasi-synchronously, returning the sign-in credentials
|
|
||||||
/// (and also adding the credentials to the state)
|
|
||||||
/// This returns a Credentials object
|
|
||||||
export async function authenticate_await_completion() {
|
|
||||||
return await invoke('plugin:auth|auth_authenticate_await_completion')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cancel_flow() {
|
|
||||||
return await invoke('plugin:auth|auth_cancel_flow')
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Refresh some credentials using Hydra, if needed
|
|
||||||
/// user is UUID
|
/// user is UUID
|
||||||
/// update_name is bool
|
export async function get_default_user() {
|
||||||
/// Returns a Credentials object
|
return await invoke('plugin:auth|auth_get_default_user')
|
||||||
export async function refresh(user, update_name) {
|
}
|
||||||
return await invoke('plugin:auth|auth_refresh', { user, update_name })
|
|
||||||
|
/// Updates the default user
|
||||||
|
/// user is UUID
|
||||||
|
export async function set_default_user(user) {
|
||||||
|
return await invoke('plugin:auth|auth_set_default_user', { user })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a user account from the database
|
/// Remove a user account from the database
|
||||||
@ -48,13 +40,6 @@ export async function remove_user(user) {
|
|||||||
return await invoke('plugin:auth|auth_remove_user', { user })
|
return await invoke('plugin:auth|auth_remove_user', { user })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a path as a profile in-memory
|
|
||||||
// user is UUID
|
|
||||||
/// Returns a bool
|
|
||||||
export async function has_user(user) {
|
|
||||||
return await invoke('plugin:auth|auth_has_user', { user })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a list of users
|
/// Returns a list of users
|
||||||
/// Returns an Array of Credentials
|
/// Returns an Array of Credentials
|
||||||
export async function users() {
|
export async function users() {
|
||||||
|
|||||||
21
theseus_gui/src/store/error.js
Normal file
21
theseus_gui/src/store/error.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useError = defineStore('errorsStore', {
|
||||||
|
state: () => ({
|
||||||
|
errorModal: null,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setErrorModal(ref) {
|
||||||
|
this.errorModal = ref
|
||||||
|
},
|
||||||
|
showError(error) {
|
||||||
|
this.errorModal.show(error)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const handleSevereError = (err) => {
|
||||||
|
const error = useError()
|
||||||
|
error.showError(err)
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
@ -14,15 +14,20 @@ use theseus::profile::create::profile_create;
|
|||||||
// 3) call the authenticate_await_complete_flow() function to get the credentials (like you would in the frontend)
|
// 3) call the authenticate_await_complete_flow() function to get the credentials (like you would in the frontend)
|
||||||
pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||||
println!("A browser window will now open, follow the login flow there.");
|
println!("A browser window will now open, follow the login flow there.");
|
||||||
let login = auth::authenticate_begin_flow().await?;
|
let login = minecraft_auth::begin_login().await?;
|
||||||
|
|
||||||
println!("URL {}", login.verification_uri.as_str());
|
println!("URL {}", login.redirect_uri.as_str());
|
||||||
println!("Code {}", login.user_code.as_str());
|
webbrowser::open(login.redirect_uri.as_str())?;
|
||||||
webbrowser::open(login.verification_uri.as_str())
|
|
||||||
.map_err(|e| IOError::with_path(e, login.verification_uri.as_str()))?;
|
|
||||||
|
|
||||||
let credentials = auth::authenticate_await_complete_flow().await?;
|
println!("Please enter URL code: ");
|
||||||
State::sync().await?;
|
let mut input = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut input)
|
||||||
|
.expect("error: unable to read user input");
|
||||||
|
|
||||||
|
println!("You entered: {}", input.trim());
|
||||||
|
|
||||||
|
let credentials = minecraft_auth::finish_login(&input, login).await?;
|
||||||
|
|
||||||
println!("Logged in user {}.", credentials.username);
|
println!("Logged in user {}.", credentials.username);
|
||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
@ -38,6 +43,11 @@ async fn main() -> theseus::Result<()> {
|
|||||||
let st = State::get().await?;
|
let st = State::get().await?;
|
||||||
//State::update();
|
//State::update();
|
||||||
|
|
||||||
|
if minecraft_auth::users().await?.is_empty() {
|
||||||
|
println!("No users found, authenticating.");
|
||||||
|
authenticate_run().await?; // could take credentials from here direct, but also deposited in state users
|
||||||
|
}
|
||||||
|
|
||||||
// Autodetect java globals
|
// Autodetect java globals
|
||||||
let jres = jre::get_all_jre().await?;
|
let jres = jre::get_all_jre().await?;
|
||||||
let java_8 = jre::find_filtered_jres("1.8", jres.clone(), false).await?;
|
let java_8 = jre::find_filtered_jres("1.8", jres.clone(), false).await?;
|
||||||
@ -91,12 +101,6 @@ async fn main() -> theseus::Result<()> {
|
|||||||
|
|
||||||
State::sync().await?;
|
State::sync().await?;
|
||||||
|
|
||||||
// Attempt to run game
|
|
||||||
if auth::users().await?.is_empty() {
|
|
||||||
println!("No users found, authenticating.");
|
|
||||||
authenticate_run().await?; // could take credentials from here direct, but also deposited in state users
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("running");
|
println!("running");
|
||||||
// Run a profile, running minecraft and store the RwLock to the process
|
// Run a profile, running minecraft and store the RwLock to the process
|
||||||
let proc_lock = profile::run(&profile_path).await?;
|
let proc_lock = profile::run(&profile_path).await?;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user