Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c4d935d90 |
No files matched your search
@@ -2,8 +2,5 @@
|
|||||||
[target.'cfg(windows)']
|
[target.'cfg(windows)']
|
||||||
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
rustflags = ["-C", "link-args=/STACK:16777220", "--cfg", "tokio_unstable"]
|
||||||
|
|
||||||
[target.x86_64-pc-windows-msvc]
|
|
||||||
linker = "rust-lld"
|
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
rustflags = ["--cfg", "tokio_unstable"]
|
rustflags = ["--cfg", "tokio_unstable"]
|
||||||
@@ -19,29 +19,26 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
- name: Fetch docker metadata
|
- name: Fetch docker metadata
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v3
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/modrinth/daedalus
|
images: ghcr.io/modrinth/daedalus
|
||||||
- name: Login to GitHub Images
|
- name: Login to GitHub Images
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
file: ./apps/daedalus_client/Dockerfile
|
file: ./apps/daedalus_client/Dockerfile
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
cache-from: type=registry,ref=ghcr.io/modrinth/daedalus:main
|
|
||||||
cache-to: type=inline
|
|
||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
run-docker:
|
run-docker:
|
||||||
if: github.repository_owner == 'modrinth'
|
if: github.repository_owner == 'modrinth'
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
wait:
|
wait:
|
||||||
if: github.repository_owner == 'modrinth'
|
if: github.repository_owner == 'modrinth'
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
deployments: write
|
deployments: write
|
||||||
|
|||||||
@@ -17,29 +17,26 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
- name: Fetch docker metadata
|
- name: Fetch docker metadata
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v3
|
||||||
with:
|
with:
|
||||||
images: ghcr.io/modrinth/labrinth
|
images: ghcr.io/modrinth/labrinth
|
||||||
- name: Login to GitHub Images
|
- name: Login to GitHub Images
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
file: ./apps/labrinth/Dockerfile
|
file: ./apps/labrinth/Dockerfile
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||||
cache-from: type=registry,ref=ghcr.io/modrinth/labrinth:main
|
|
||||||
cache-to: type=inline
|
|
||||||
@@ -28,13 +28,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [macos-latest, windows-latest, ubuntu-22.04]
|
platform: [macos-latest, windows-latest, blacksmith-4vcpu-ubuntu-2204]
|
||||||
include:
|
include:
|
||||||
- platform: macos-latest
|
- platform: macos-latest
|
||||||
artifact-target-name: universal-apple-darwin
|
artifact-target-name: universal-apple-darwin
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
artifact-target-name: x86_64-pc-windows-msvc
|
artifact-target-name: x86_64-pc-windows-msvc
|
||||||
- platform: ubuntu-22.04
|
- platform: blacksmith-4vcpu-ubuntu-2204
|
||||||
artifact-target-name: x86_64-unknown-linux-gnu
|
artifact-target-name: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||||
chmod: 0755
|
chmod: 0755
|
||||||
|
|
||||||
- name: ⚙️ Set application version and environment
|
- name: ⚙️ Set application version
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||||
@@ -84,8 +84,6 @@ jobs:
|
|||||||
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||||
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
||||||
|
|
||||||
cp packages/app-lib/.env.prod packages/app-lib/.env
|
|
||||||
|
|
||||||
- name: 💨 Setup Turbo cache
|
- name: 💨 Setup Turbo cache
|
||||||
uses: rharkor/caching-for-turbo@v1.8
|
uses: rharkor/caching-for-turbo@v1.8
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release Modrinth App
|
name: Release Modrinth App
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||||
|
|
||||||
env:
|
env:
|
||||||
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
|
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Lint and Test
|
name: Lint and Test
|
||||||
runs-on: ubuntu-22.04
|
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Ensure pnpm output is colored in GitHub Actions logs
|
# Ensure pnpm output is colored in GitHub Actions logs
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
# cargo-binstall does not have pre-built binaries for sqlx-cli, so we fall
|
||||||
# back to a cached cargo install
|
# back to a cached cargo install
|
||||||
- name: 🧰 Setup cargo-sqlx
|
- name: 🧰 Setup cargo-sqlx
|
||||||
uses: taiki-e/cache-cargo-install-action@v2
|
uses: AlexTMjugador/cache-cargo-install-action@feat/features-support
|
||||||
with:
|
with:
|
||||||
tool: sqlx-cli
|
tool: sqlx-cli
|
||||||
locked: false
|
locked: false
|
||||||
@@ -74,14 +74,10 @@ jobs:
|
|||||||
cp .env.local .env
|
cp .env.local .env
|
||||||
sqlx database setup
|
sqlx database setup
|
||||||
|
|
||||||
- name: ⚙️ Set app environment
|
|
||||||
working-directory: packages/app-lib
|
|
||||||
run: cp .env.staging .env
|
|
||||||
|
|
||||||
- name: 🔍 Lint and test
|
- name: 🔍 Lint and test
|
||||||
run: pnpm run ci
|
run: pnpm run ci
|
||||||
|
|
||||||
- name: 🔍 Verify intl:extract has been run
|
- name: 🔍 Verify intl:extract has been run
|
||||||
run: |
|
run: |
|
||||||
pnpm intl:extract
|
pnpm intl:extract
|
||||||
git diff --exit-code --color */*/src/locales/en-US/index.json
|
git diff --exit-code */*/src/locales/en-US/index.json
|
||||||
Generated
+1
@@ -10,6 +10,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
<sourceFolder url="file://$MODULE_DIR$/apps/labrinth/tests" isTestSource="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/app-lib/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/packages/rust-common/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/packages/ariadne/src" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
|
|||||||
Vendored
+1
-2
@@ -4,7 +4,6 @@
|
|||||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||||
"editor.detectIndentation": true,
|
"editor.detectIndentation": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit"
|
||||||
"source.organizeImports": "always",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
-47
@@ -5731,17 +5731,6 @@ dependencies = [
|
|||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
|
||||||
dependencies = [
|
|
||||||
"phf_macros 0.12.1",
|
|
||||||
"phf_shared 0.12.1",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_codegen"
|
name = "phf_codegen"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -5792,16 +5781,6 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_generator"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
|
|
||||||
dependencies = [
|
|
||||||
"fastrand 2.3.0",
|
|
||||||
"phf_shared 0.12.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_macros"
|
name = "phf_macros"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -5829,19 +5808,6 @@ dependencies = [
|
|||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_macros"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368"
|
|
||||||
dependencies = [
|
|
||||||
"phf_generator 0.12.1",
|
|
||||||
"phf_shared 0.12.1",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.101",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_shared"
|
name = "phf_shared"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -5869,15 +5835,6 @@ dependencies = [
|
|||||||
"siphasher 1.0.1",
|
"siphasher 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "phf_shared"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
|
|
||||||
dependencies = [
|
|
||||||
"siphasher 1.0.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@@ -8973,7 +8930,6 @@ dependencies = [
|
|||||||
"data-url",
|
"data-url",
|
||||||
"dirs",
|
"dirs",
|
||||||
"discord-rich-presence",
|
"discord-rich-presence",
|
||||||
"dotenvy",
|
|
||||||
"dunce",
|
"dunce",
|
||||||
"either",
|
"either",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
@@ -8989,7 +8945,6 @@ dependencies = [
|
|||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
"p256",
|
"p256",
|
||||||
"paste",
|
"paste",
|
||||||
"phf 0.12.1",
|
|
||||||
"png",
|
"png",
|
||||||
"quartz_nbt",
|
"quartz_nbt",
|
||||||
"quick-xml 0.37.5",
|
"quick-xml 0.37.5",
|
||||||
@@ -9029,8 +8984,6 @@ dependencies = [
|
|||||||
"dashmap",
|
"dashmap",
|
||||||
"either",
|
"either",
|
||||||
"enumset",
|
"enumset",
|
||||||
"hyper 1.6.0",
|
|
||||||
"hyper-util",
|
|
||||||
"native-dialog",
|
"native-dialog",
|
||||||
"paste",
|
"paste",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ heck = "0.5.0"
|
|||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper = "1.6.0"
|
|
||||||
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||||
"http1",
|
"http1",
|
||||||
"native-tokio",
|
"native-tokio",
|
||||||
@@ -99,7 +98,6 @@ notify = { version = "8.0.0", default-features = false }
|
|||||||
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
notify-debouncer-mini = { version = "0.6.0", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
phf = { version = "0.12.1", features = ["macros"] }
|
|
||||||
png = "0.17.16"
|
png = "0.17.16"
|
||||||
prometheus = "0.14.0"
|
prometheus = "0.14.0"
|
||||||
quartz_nbt = "0.2.9"
|
quartz_nbt = "0.2.9"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"tsc:check": "vue-tsc --noEmit",
|
"tsc:check": "vue-tsc --noEmit",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/helpers,src/pages,src/store}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
"test": "vue-tsc --noEmit"
|
"test": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,35 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
import { computed, onMounted, onUnmounted, ref, watch, provide } from 'vue'
|
||||||
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
|
||||||
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
|
||||||
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
|
||||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
|
||||||
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
|
|
||||||
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
|
||||||
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
|
||||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
|
||||||
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
|
||||||
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
|
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
|
||||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
|
||||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
|
||||||
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
|
||||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
|
||||||
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
|
||||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
|
||||||
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
|
||||||
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
|
||||||
import { get_user } from '@/helpers/cache.js'
|
|
||||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
|
||||||
import { useFetch } from '@/helpers/fetch.js'
|
|
||||||
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
|
||||||
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
|
|
||||||
import { useError } from '@/store/error.js'
|
|
||||||
import { useInstall } from '@/store/install.js'
|
|
||||||
import { useLoading, useTheming } from '@/store/state'
|
|
||||||
import {
|
import {
|
||||||
ArrowBigUpDashIcon,
|
ArrowBigUpDashIcon,
|
||||||
ChangeSkinIcon,
|
ChangeSkinIcon,
|
||||||
@@ -42,44 +13,67 @@ import {
|
|||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
MaximizeIcon,
|
MaximizeIcon,
|
||||||
MinimizeIcon,
|
MinimizeIcon,
|
||||||
NewspaperIcon,
|
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RestoreIcon,
|
RestoreIcon,
|
||||||
RightArrowIcon,
|
RightArrowIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
WorldIcon,
|
WorldIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
NewspaperIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
NewsArticleCard,
|
Notifications,
|
||||||
NotificationPanel,
|
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
provideNotificationManager,
|
NewsArticleCard,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { renderString } from '@modrinth/utils'
|
import { useLoading, useTheming } from '@/store/state'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import ModrinthAppLogo from '@/assets/modrinth_app.svg?component'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import Breadcrumbs from '@/components/ui/Breadcrumbs.vue'
|
||||||
|
import RunningAppBar from '@/components/ui/RunningAppBar.vue'
|
||||||
|
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||||
|
import ErrorModal from '@/components/ui/ErrorModal.vue'
|
||||||
|
import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
||||||
|
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||||
|
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||||
import { type } from '@tauri-apps/plugin-os'
|
import { type } from '@tauri-apps/plugin-os'
|
||||||
import { check } from '@tauri-apps/plugin-updater'
|
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
|
||||||
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||||
import { computed, onMounted, onUnmounted, provide, ref, watch } from 'vue'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
|
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
|
||||||
import { create_profile_and_install_from_file } from './helpers/pack'
|
import { create_profile_and_install_from_file } from './helpers/pack'
|
||||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
import { useError } from '@/store/error.js'
|
||||||
|
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||||
|
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
|
||||||
|
import IncompatibilityWarningModal from '@/components/ui/install_flow/IncompatibilityWarningModal.vue'
|
||||||
|
import InstallConfirmModal from '@/components/ui/install_flow/InstallConfirmModal.vue'
|
||||||
|
import { useInstall } from '@/store/install.js'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||||
|
import { saveWindowState, StateFlags } from '@tauri-apps/plugin-window-state'
|
||||||
|
import { renderString } from '@modrinth/utils'
|
||||||
|
import { useFetch } from '@/helpers/fetch.js'
|
||||||
|
import { check } from '@tauri-apps/plugin-updater'
|
||||||
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
|
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
|
||||||
|
import { get_user } from '@/helpers/cache.js'
|
||||||
|
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
|
||||||
|
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||||
|
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
||||||
|
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||||
import { AppNotificationManager } from './providers/app-notifications'
|
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const notificationManager = new AppNotificationManager()
|
|
||||||
provideNotificationManager(notificationManager)
|
|
||||||
const { handleError, addNotification } = notificationManager
|
|
||||||
|
|
||||||
const news = ref([])
|
const news = ref([])
|
||||||
|
|
||||||
const urlModal = ref(null)
|
const urlModal = ref(null)
|
||||||
@@ -172,7 +166,7 @@ async function setupApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await warning_listener((e) =>
|
await warning_listener((e) =>
|
||||||
addNotification({
|
notificationsWrapper.value.addNotification({
|
||||||
title: 'Warning',
|
title: 'Warning',
|
||||||
text: e.message,
|
text: e.message,
|
||||||
type: 'warn',
|
type: 'warn',
|
||||||
@@ -256,6 +250,9 @@ const route = useRoute()
|
|||||||
const loading = useLoading()
|
const loading = useLoading()
|
||||||
loading.setEnabled(false)
|
loading.setEnabled(false)
|
||||||
|
|
||||||
|
const notifications = useNotifications()
|
||||||
|
const notificationsWrapper = ref()
|
||||||
|
|
||||||
const error = useError()
|
const error = useError()
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
|
|
||||||
@@ -266,8 +263,6 @@ const incompatibilityWarningModal = ref()
|
|||||||
|
|
||||||
const credentials = ref()
|
const credentials = ref()
|
||||||
|
|
||||||
const modrinthLoginFlowWaitModal = ref()
|
|
||||||
|
|
||||||
async function fetchCredentials() {
|
async function fetchCredentials() {
|
||||||
const creds = await getCreds().catch(handleError)
|
const creds = await getCreds().catch(handleError)
|
||||||
if (creds && creds.user_id) {
|
if (creds && creds.user_id) {
|
||||||
@@ -277,24 +272,8 @@ async function fetchCredentials() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function signIn() {
|
async function signIn() {
|
||||||
modrinthLoginFlowWaitModal.value.show()
|
await login().catch(handleError)
|
||||||
|
await fetchCredentials()
|
||||||
try {
|
|
||||||
await login()
|
|
||||||
await fetchCredentials()
|
|
||||||
} catch (error) {
|
|
||||||
if (
|
|
||||||
typeof error === 'object' &&
|
|
||||||
typeof error['message'] === 'string' &&
|
|
||||||
error.message.includes('Login canceled')
|
|
||||||
) {
|
|
||||||
// Not really an error due to being a result of user interaction, show nothing
|
|
||||||
} else {
|
|
||||||
handleError(error)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
modrinthLoginFlowWaitModal.value.hide()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logOut() {
|
async function logOut() {
|
||||||
@@ -337,6 +316,8 @@ watch(
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
invoke('show_window')
|
invoke('show_window')
|
||||||
|
|
||||||
|
notifications.setNotifs(notificationsWrapper.value)
|
||||||
|
|
||||||
error.setErrorModal(errorModal.value)
|
error.setErrorModal(errorModal.value)
|
||||||
|
|
||||||
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
install.setIncompatibilityWarningModal(incompatibilityWarningModal)
|
||||||
@@ -421,9 +402,6 @@ function handleAuxClick(e) {
|
|||||||
<Suspense>
|
<Suspense>
|
||||||
<AppSettingsModal ref="settingsModal" />
|
<AppSettingsModal ref="settingsModal" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense>
|
|
||||||
<AuthGrantFlowWaitModal ref="modrinthLoginFlowWaitModal" @flow-cancel="cancelLogin" />
|
|
||||||
</Suspense>
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<InstanceCreationModal ref="installationModal" />
|
<InstanceCreationModal ref="installationModal" />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -657,7 +635,7 @@ function handleAuxClick(e) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<URLConfirmModal ref="urlModal" />
|
<URLConfirmModal ref="urlModal" />
|
||||||
<NotificationPanel has-sidebar />
|
<Notifications ref="notificationsWrapper" sidebar />
|
||||||
<ErrorModal ref="errorModal" />
|
<ErrorModal ref="errorModal" />
|
||||||
<ModInstallModal ref="modInstallModal" />
|
<ModInstallModal ref="modInstallModal" />
|
||||||
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
<IncompatibilityWarningModal ref="incompatibilityWarningModal" />
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { computed, ref } from 'vue'
|
||||||
import { duplicate, remove } from '@/helpers/profile.js'
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
EyeIcon,
|
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SearchIcon,
|
|
||||||
StopCircleIcon,
|
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
EyeIcon,
|
||||||
|
SearchIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
|
import { Button, DropdownSelect } from '@modrinth/ui'
|
||||||
import { formatCategoryHeader } from '@modrinth/utils'
|
import { formatCategoryHeader } from '@modrinth/utils'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, ref } from 'vue'
|
import { duplicate, remove } from '@/helpers/profile.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
const { handleError } = injectNotificationManager()
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instances: {
|
instances: {
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import Instance from '@/components/ui/Instance.vue'
|
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process.js'
|
|
||||||
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import {
|
import {
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
DownloadIcon,
|
|
||||||
ExternalIcon,
|
|
||||||
EyeIcon,
|
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
GlobeIcon,
|
|
||||||
PlayIcon,
|
PlayIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
StopCircleIcon,
|
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
EyeIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import Instance from '@/components/ui/Instance.vue'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import ProjectCard from '@/components/ui/ProjectCard.vue'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { duplicate, kill, remove, run } from '@/helpers/profile.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
const { handleError } = injectNotificationManager()
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { HeadingLink } from '@modrinth/ui'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -73,23 +73,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
get_default_user,
|
users,
|
||||||
login as login_flow,
|
|
||||||
remove_user,
|
remove_user,
|
||||||
set_default_user,
|
set_default_user,
|
||||||
users,
|
login as login_flow,
|
||||||
|
get_default_user,
|
||||||
} from '@/helpers/auth'
|
} from '@/helpers/auth'
|
||||||
|
import { handleError } from '@/store/state.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { process_listener } from '@/helpers/events'
|
||||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
|
||||||
import { get_available_skins } from '@/helpers/skins'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
import { handleSevereError } from '@/store/error.js'
|
||||||
import { DropdownIcon, LogInIcon, PlusIcon, SpinnerIcon, TrashIcon } from '@modrinth/assets'
|
import { get_available_skins } from '@/helpers/skins'
|
||||||
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||||
import { computed, onBeforeUnmount, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
mode: {
|
mode: {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { add_project_from_path } from '@/helpers/profile.js'
|
import { DropdownIcon, PlusIcon, FolderOpenIcon } from '@modrinth/assets'
|
||||||
import { DropdownIcon, FolderOpenIcon, PlusIcon } from '@modrinth/assets'
|
import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||||
import { ButtonStyled, injectNotificationManager, OverflowMenu } from '@modrinth/ui'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { add_project_from_path } from '@/helpers/profile.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ChatIcon } from '@/assets/icons'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
|
||||||
import { install } from '@/helpers/profile.js'
|
|
||||||
import { cancel_directory_change } from '@/helpers/settings.ts'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CopyIcon,
|
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
|
XIcon,
|
||||||
HammerIcon,
|
HammerIcon,
|
||||||
LogInIcon,
|
LogInIcon,
|
||||||
UpdatedIcon,
|
UpdatedIcon,
|
||||||
XIcon,
|
CopyIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { ButtonStyled, Collapsible, injectNotificationManager } from '@modrinth/ui'
|
import { ChatIcon } from '@/assets/icons'
|
||||||
import { computed, ref } from 'vue'
|
import { ButtonStyled, Collapsible } from '@modrinth/ui'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
const { handleError } = injectNotificationManager()
|
import { login as login_flow, set_default_user } from '@/helpers/auth.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
import { cancel_directory_change } from '@/helpers/settings.ts'
|
||||||
|
import { install } from '@/helpers/profile.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
const error = ref()
|
const error = ref()
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { XIcon, PlusIcon } from '@modrinth/assets'
|
||||||
|
import { Button, Checkbox } from '@modrinth/ui'
|
||||||
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
import { PackageIcon, VersionIcon } from '@/assets/icons'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
|
||||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import { Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { export_profile_mrpack, get_pack_export_candidates } from '@/helpers/profile.js'
|
||||||
const { handleError } = injectNotificationManager()
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
instance: {
|
instance: {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { process_listener } from '@/helpers/events'
|
import { useRouter } from 'vue-router'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
|
||||||
import { finish_install, kill, run } from '@/helpers/profile'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
GameIcon,
|
GameIcon,
|
||||||
@@ -13,13 +9,17 @@ import {
|
|||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TimerIcon,
|
TimerIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, ButtonStyled, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
import { Avatar, ButtonStyled, useRelativeTime } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { finish_install, kill, run } from '@/helpers/profile'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { process_listener } from '@/helpers/events'
|
||||||
|
import { handleError } from '@/store/state.js'
|
||||||
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -198,17 +198,6 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import {
|
|
||||||
get_default_launcher_path,
|
|
||||||
get_importable_instances,
|
|
||||||
import_instance,
|
|
||||||
} from '@/helpers/import.js'
|
|
||||||
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
|
||||||
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
|
||||||
import { create } from '@/helpers/profile'
|
|
||||||
import { get_loaders } from '@/helpers/tags'
|
|
||||||
import {
|
import {
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
@@ -219,14 +208,24 @@ import {
|
|||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Checkbox, Chips, injectNotificationManager } from '@modrinth/ui'
|
import { Avatar, Button, Checkbox, Chips } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
||||||
|
import { get_loaders } from '@/helpers/tags'
|
||||||
|
import { create } from '@/helpers/profile'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
const { handleError } = injectNotificationManager()
|
import { create_profile_and_install_from_file } from '@/helpers/pack.js'
|
||||||
|
import {
|
||||||
|
get_default_launcher_path,
|
||||||
|
get_importable_instances,
|
||||||
|
import_instance,
|
||||||
|
} from '@/helpers/import.js'
|
||||||
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
|
|
||||||
const profile_name = ref('')
|
const profile_name = ref('')
|
||||||
const game_version = ref('')
|
const game_version = ref('')
|
||||||
@@ -306,16 +305,12 @@ const [
|
|||||||
get_game_versions().then(shallowRef).catch(handleError),
|
get_game_versions().then(shallowRef).catch(handleError),
|
||||||
get_loaders()
|
get_loaders()
|
||||||
.then((value) =>
|
.then((value) =>
|
||||||
ref(
|
value
|
||||||
value
|
.filter((item) => item.supported_project_types.includes('modpack'))
|
||||||
.filter((item) => item.supported_project_types.includes('modpack'))
|
.map((item) => item.name.toLowerCase()),
|
||||||
.map((item) => item.name.toLowerCase()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.then(ref)
|
||||||
handleError(err)
|
.catch(handleError),
|
||||||
return ref([])
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
loaders.value.unshift('vanilla')
|
loaders.value.unshift('vanilla')
|
||||||
|
|
||||||
|
|||||||
@@ -35,14 +35,13 @@
|
|||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import { PlusIcon, CheckIcon, XIcon } from '@modrinth/assets'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { Button } from '@modrinth/ui'
|
||||||
import { find_filtered_jres } from '@/helpers/jre.js'
|
|
||||||
import { CheckIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { find_filtered_jres } from '@/helpers/jre.js'
|
||||||
const { handleError } = injectNotificationManager()
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
const chosenInstallOptions = ref([])
|
const chosenInstallOptions = ref([])
|
||||||
const detectJavaModal = ref(null)
|
const detectJavaModal = ref(null)
|
||||||
|
|||||||
@@ -52,22 +52,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
FolderSearchIcon,
|
|
||||||
PlayIcon,
|
|
||||||
SearchIcon,
|
SearchIcon,
|
||||||
|
PlayIcon,
|
||||||
|
CheckIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
FolderSearchIcon,
|
||||||
|
DownloadIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
import { Button } from '@modrinth/ui'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { auto_install_java, find_filtered_jres, get_jre, test_jre } from '@/helpers/jre.js'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
const { handleError } = injectNotificationManager()
|
import JavaDetectionModal from '@/components/ui/JavaDetectionModal.vue'
|
||||||
|
import { handleError } from '@/store/state.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
version: {
|
version: {
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const featuredCategory = computed(() => {
|
const featuredCategory = computed(() => {
|
||||||
if (props.project.display_categories.includes('optimization')) {
|
if (props.project.categories.includes('optimization')) {
|
||||||
return 'optimization'
|
return 'optimization'
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.project.display_categories[0] ?? props.project.categories[0]
|
if (props.project.categories.length > 0) {
|
||||||
|
return props.project.categories[0]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
const toColor = computed(() => {
|
const toColor = computed(() => {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
|
||||||
import { list } from '@/helpers/profile'
|
import { list } from '@/helpers/profile'
|
||||||
import { SpinnerIcon } from '@modrinth/assets'
|
import { handleError } from '@/store/notifications'
|
||||||
import { Avatar, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { onUnmounted, ref } from 'vue'
|
import { onUnmounted, ref } from 'vue'
|
||||||
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
const { handleError } = injectNotificationManager()
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
|
import { Avatar } from '@modrinth/ui'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { SpinnerIcon } from '@modrinth/assets'
|
||||||
|
|
||||||
const recentInstances = ref([])
|
const recentInstances = ref([])
|
||||||
const getInstances = async () => {
|
const getInstances = async () => {
|
||||||
|
|||||||
@@ -94,24 +94,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { loading_listener, process_listener } from '@/helpers/events'
|
|
||||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
|
||||||
import { get_many } from '@/helpers/profile.js'
|
|
||||||
import { progress_bars_list } from '@/helpers/state.js'
|
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
DropdownIcon,
|
|
||||||
StopCircleIcon,
|
StopCircleIcon,
|
||||||
TerminalSquareIcon,
|
TerminalSquareIcon,
|
||||||
|
DropdownIcon,
|
||||||
UnplugIcon,
|
UnplugIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Button, ButtonStyled, Card, injectNotificationManager } from '@modrinth/ui'
|
import { Button, ButtonStyled, Card } from '@modrinth/ui'
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||||
|
import { loading_listener, process_listener } from '@/helpers/events'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { progress_bars_list } from '@/helpers/state.js'
|
||||||
const { handleError } = injectNotificationManager()
|
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { get_many } from '@/helpers/profile.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const card = ref(null)
|
const card = ref(null)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import { Button } from '@modrinth/ui'
|
||||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
|
||||||
import { get_project, get_version } from '@/helpers/cache.js'
|
|
||||||
import { get_categories } from '@/helpers/tags.js'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import SearchCard from '@/components/ui/SearchCard.vue'
|
||||||
const { handleError } = injectNotificationManager()
|
import { get_categories } from '@/helpers/tags.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { get_version, get_project } from '@/helpers/cache.js'
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
const confirmModal = ref(null)
|
const confirmModal = ref(null)
|
||||||
const project = ref(null)
|
const project = ref(null)
|
||||||
|
|||||||
@@ -1,29 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import { Avatar, ButtonStyled, OverflowMenu, useRelativeTime } from '@modrinth/ui'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { get_user_many } from '@/helpers/cache'
|
|
||||||
import { friend_listener } from '@/helpers/events'
|
|
||||||
import { add_friend, friend_statuses, friends, remove_friend } from '@/helpers/friends'
|
|
||||||
import {
|
import {
|
||||||
MailIcon,
|
UserPlusIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
|
MailIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
UserPlusIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import { ref, onUnmounted, watch, computed } from 'vue'
|
||||||
Avatar,
|
import { friend_listener } from '@/helpers/events'
|
||||||
ButtonStyled,
|
import { friends, friend_statuses, add_friend, remove_friend } from '@/helpers/friends'
|
||||||
injectNotificationManager,
|
import { get_user_many } from '@/helpers/cache'
|
||||||
OverflowMenu,
|
import { handleError } from '@/store/notifications.js'
|
||||||
useRelativeTime,
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
} from '@modrinth/ui'
|
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -57,16 +57,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { XIcon, DownloadIcon } from '@modrinth/assets'
|
||||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
import { Button } from '@modrinth/ui'
|
||||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { formatCategory } from '@modrinth/utils'
|
import { formatCategory } from '@modrinth/utils'
|
||||||
|
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { handleError } from '@/store/state.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import Multiselect from 'vue-multiselect'
|
import Multiselect from 'vue-multiselect'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
|
|
||||||
const instance = ref(null)
|
const instance = ref(null)
|
||||||
const project = ref(null)
|
const project = ref(null)
|
||||||
const versions = ref(null)
|
const versions = ref(null)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
|
||||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
import { Button } from '@modrinth/ui'
|
||||||
|
import { create_profile_and_install as pack_install } from '@/helpers/pack'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
const { handleError } = injectNotificationManager()
|
import { handleError } from '@/store/state.js'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
const versionId = ref()
|
const versionId = ref()
|
||||||
const project = ref()
|
const project = ref()
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import {
|
import {
|
||||||
check_installed,
|
|
||||||
create,
|
|
||||||
get,
|
|
||||||
add_project_from_version as installMod,
|
|
||||||
list,
|
|
||||||
} from '@/helpers/profile'
|
|
||||||
import { installVersionDependencies } from '@/store/install.js'
|
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RightArrowIcon,
|
|
||||||
UploadIcon,
|
UploadIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
RightArrowIcon,
|
||||||
|
CheckIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Card, injectNotificationManager } from '@modrinth/ui'
|
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import {
|
||||||
|
add_project_from_version as installMod,
|
||||||
|
check_installed,
|
||||||
|
get,
|
||||||
|
list,
|
||||||
|
create,
|
||||||
|
} from '@/helpers/profile'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { installVersionDependencies } from '@/store/install.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const versions = ref()
|
const versions = ref()
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
|
||||||
import { CopyIcon, EditIcon, PlusIcon, SpinnerIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
ButtonStyled,
|
|
||||||
Checkbox,
|
|
||||||
injectNotificationManager,
|
|
||||||
OverflowMenu,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { SpinnerIcon, TrashIcon, UploadIcon, PlusIcon, EditIcon, CopyIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, OverflowMenu, Checkbox } from '@modrinth/ui'
|
||||||
|
import { computed, ref, type Ref, watch } from 'vue'
|
||||||
|
import { duplicate, edit, edit_icon, list, remove } from '@/helpers/profile'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { GameInstance, InstanceSettingsTabProps } from '../../../helpers/types'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import type { InstanceSettingsTabProps, GameInstance } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { edit } from '@/helpers/profile'
|
import { Checkbox } from '@modrinth/ui'
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { Checkbox, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import type { AppSettings, Hooks, InstanceSettingsTabProps } from '../../../helpers/types'
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
|
import type { InstanceSettingsTabProps, AppSettings, Hooks } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|||||||
@@ -1,30 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
|
||||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { get_project, get_version_many } from '@/helpers/cache'
|
|
||||||
import { get_loader_versions } from '@/helpers/metadata'
|
|
||||||
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
|
||||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
|
||||||
HammerIcon,
|
|
||||||
IssuesIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
TransferIcon,
|
TransferIcon,
|
||||||
UndoIcon,
|
IssuesIcon,
|
||||||
UnlinkIcon,
|
HammerIcon,
|
||||||
UnplugIcon,
|
DownloadIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
|
UndoIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
UnplugIcon,
|
||||||
|
UnlinkIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import { Avatar, Checkbox, Chips, ButtonStyled, TeleportDropdownMenu } from '@modrinth/ui'
|
||||||
Avatar,
|
import { computed, type ComputedRef, type Ref, ref, shallowRef, watch } from 'vue'
|
||||||
ButtonStyled,
|
import { edit, install, update_repair_modrinth } from '@/helpers/profile'
|
||||||
Checkbox,
|
import { handleError } from '@/store/notifications'
|
||||||
Chips,
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
TeleportDropdownMenu,
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
injectNotificationManager,
|
import { get_loader_versions } from '@/helpers/metadata'
|
||||||
} from '@modrinth/ui'
|
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
import {
|
import {
|
||||||
formatCategory,
|
formatCategory,
|
||||||
type GameVersionTag,
|
type GameVersionTag,
|
||||||
@@ -32,16 +25,16 @@ import {
|
|||||||
type Project,
|
type Project,
|
||||||
type Version,
|
type Version,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { get_project, get_version_many } from '@/helpers/cache'
|
||||||
|
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, ref, shallowRef, watch, type ComputedRef, type Ref } from 'vue'
|
|
||||||
import type {
|
import type {
|
||||||
InstanceSettingsTabProps,
|
InstanceSettingsTabProps,
|
||||||
Manifest,
|
|
||||||
ManifestLoaderVersion,
|
ManifestLoaderVersion,
|
||||||
|
Manifest,
|
||||||
} from '../../../helpers/types'
|
} from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const repairConfirmModal = ref()
|
const repairConfirmModal = ref()
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
import { Checkbox, Slider } from '@modrinth/ui'
|
||||||
import useMemorySlider from '@/composables/useMemorySlider'
|
|
||||||
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
import { CheckCircleIcon, XCircleIcon } from '@modrinth/assets'
|
||||||
import { Checkbox, injectNotificationManager, Slider } from '@modrinth/ui'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import { computed, readonly, ref, watch } from 'vue'
|
import { computed, readonly, ref, watch } from 'vue'
|
||||||
import type { AppSettings, InstanceSettingsTabProps, MemorySettings } from '../../../helpers/types'
|
import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||||
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { edit } from '@/helpers/profile'
|
import { Checkbox, Toggle } from '@modrinth/ui'
|
||||||
import { get } from '@/helpers/settings.ts'
|
|
||||||
import { Checkbox, injectNotificationManager, Toggle } from '@modrinth/ui'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import { computed, ref, type Ref, watch } from 'vue'
|
import { computed, ref, type Ref, watch } from 'vue'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { get } from '@/helpers/settings.ts'
|
||||||
|
import { edit } from '@/helpers/profile'
|
||||||
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
import type { AppSettings, InstanceSettingsTabProps } from '../../../helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<InstanceSettingsTabProps>()
|
const props = defineProps<InstanceSettingsTabProps>()
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
onFlowCancel: {
|
|
||||||
type: Function,
|
|
||||||
default() {
|
|
||||||
return async () => {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const modal = ref()
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
modal.value.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
modal.value.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ show, hide })
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<ModalWrapper ref="modal" @hide="onFlowCancel">
|
|
||||||
<template #title>
|
|
||||||
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
|
|
||||||
<LogInIcon /> Sign in
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="flex justify-center gap-2">
|
|
||||||
<SpinnerIcon class="w-12 h-12 animate-spin" />
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-secondary">
|
|
||||||
Please sign in at the browser window that just opened to continue.
|
|
||||||
</p>
|
|
||||||
</ModalWrapper>
|
|
||||||
</template>
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
|
||||||
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { get_java_versions, set_java_version } from '@/helpers/jre'
|
||||||
const { handleError } = injectNotificationManager()
|
import { handleError } from '@/store/notifications'
|
||||||
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
|
||||||
const javaVersions = ref(await get_java_versions().catch(handleError))
|
const javaVersions = ref(await get_java_versions().catch(handleError))
|
||||||
async function updateJavaVersion(version) {
|
async function updateJavaVersion(version) {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { Button, Slider } from '@modrinth/ui'
|
||||||
import { purge_cache_types } from '@/helpers/cache.js'
|
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
|
||||||
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
|
||||||
import { Button, Slider, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
import { purge_cache_types } from '@/helpers/cache.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { BoxIcon, FolderSearchIcon, TrashIcon } from '@modrinth/assets'
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const settings = ref(await get())
|
const settings = ref(await get())
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -100,39 +100,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import { ref, computed, watch, useTemplateRef } from 'vue'
|
||||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
|
||||||
import {
|
|
||||||
add_and_equip_custom_skin,
|
|
||||||
determineModelType,
|
|
||||||
get_normalized_skin_texture,
|
|
||||||
remove_custom_skin,
|
|
||||||
unequip_skin,
|
|
||||||
type Cape,
|
|
||||||
type Skin,
|
|
||||||
type SkinModel,
|
|
||||||
} from '@/helpers/skins.ts'
|
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
SaveIcon,
|
|
||||||
SpinnerIcon,
|
|
||||||
UploadIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@modrinth/assets'
|
|
||||||
import {
|
import {
|
||||||
|
SkinPreviewRenderer,
|
||||||
Button,
|
Button,
|
||||||
ButtonStyled,
|
RadioButtons,
|
||||||
CapeButton,
|
CapeButton,
|
||||||
CapeLikeTextButton,
|
CapeLikeTextButton,
|
||||||
injectNotificationManager,
|
ButtonStyled,
|
||||||
RadioButtons,
|
|
||||||
SkinPreviewRenderer,
|
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
import {
|
||||||
|
add_and_equip_custom_skin,
|
||||||
const { handleError } = injectNotificationManager()
|
remove_custom_skin,
|
||||||
|
unequip_skin,
|
||||||
|
type Skin,
|
||||||
|
type Cape,
|
||||||
|
type SkinModel,
|
||||||
|
get_normalized_skin_texture,
|
||||||
|
determineModelType,
|
||||||
|
} from '@/helpers/skins.ts'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import {
|
||||||
|
UploadIcon,
|
||||||
|
CheckIcon,
|
||||||
|
SaveIcon,
|
||||||
|
XIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
SpinnerIcon,
|
||||||
|
} from '@modrinth/assets'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||||
|
|
||||||
const modal = useTemplateRef('modal')
|
const modal = useTemplateRef('modal')
|
||||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onBeforeUnmount, watch } from 'vue'
|
||||||
|
import { UploadIcon } from '@modrinth/assets'
|
||||||
|
import { useNotifications } from '@/store/state'
|
||||||
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_dragged_skin_data } from '@/helpers/skins'
|
import { get_dragged_skin_data } from '@/helpers/skins'
|
||||||
import { UploadIcon } from '@modrinth/assets'
|
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
|
||||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const notifications = useNotifications()
|
||||||
|
|
||||||
const modal = ref()
|
const modal = ref()
|
||||||
const fileInput = ref<HTMLInputElement>()
|
const fileInput = ref<HTMLInputElement>()
|
||||||
@@ -99,7 +99,7 @@ async function setupDragDropListener() {
|
|||||||
const data = await get_dragged_skin_data(filePath)
|
const data = await get_dragged_skin_data(filePath)
|
||||||
await processData(data.buffer)
|
await processData(data.buffer)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addNotification({
|
notifications.addNotification({
|
||||||
title: 'Error processing file',
|
title: 'Error processing file',
|
||||||
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
text: error instanceof Error ? error.message : 'Failed to read the dropped file.',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import type { Dayjs } from 'dayjs'
|
||||||
import { get_project } from '@/helpers/cache'
|
import dayjs from 'dayjs'
|
||||||
import { process_listener } from '@/helpers/events'
|
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
|
||||||
import { kill, run } from '@/helpers/profile'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils'
|
|
||||||
import { handleSevereError } from '@/store/error'
|
|
||||||
import {
|
import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FolderOpenIcon,
|
FolderOpenIcon,
|
||||||
@@ -19,20 +13,25 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
injectNotificationManager,
|
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
SmartClickable,
|
SmartClickable,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { capitalizeString } from '@modrinth/utils'
|
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
|
||||||
import { useVIntl } from '@vintl/vintl'
|
import { useVIntl } from '@vintl/vintl'
|
||||||
import type { Dayjs } from 'dayjs'
|
import { computed, nextTick, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import dayjs from 'dayjs'
|
import { showProfileInFolder } from '@/helpers/utils'
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { get_project } from '@/helpers/cache'
|
||||||
|
import { capitalizeString } from '@modrinth/utils'
|
||||||
|
import { kill, run } from '@/helpers/profile'
|
||||||
|
import { handleSevereError } from '@/store/error'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { process_listener } from '@/helpers/events'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
|
||||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { process_listener, profile_listener } from '@/helpers/events'
|
|
||||||
import { get_all } from '@/helpers/process'
|
|
||||||
import { kill, run } from '@/helpers/profile'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import {
|
import {
|
||||||
type ProtocolVersion,
|
|
||||||
type ServerData,
|
|
||||||
type ServerWorld,
|
type ServerWorld,
|
||||||
|
type ServerData,
|
||||||
type WorldWithProfile,
|
type WorldWithProfile,
|
||||||
|
get_recent_worlds,
|
||||||
getWorldIdentifier,
|
getWorldIdentifier,
|
||||||
get_profile_protocol_version,
|
get_profile_protocol_version,
|
||||||
get_recent_worlds,
|
|
||||||
refreshServerData,
|
refreshServerData,
|
||||||
start_join_server,
|
start_join_server,
|
||||||
start_join_singleplayer_world,
|
start_join_singleplayer_world,
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import { handleSevereError } from '@/store/error'
|
import { HeadingLink, GAME_MODES } from '@modrinth/ui'
|
||||||
import { useTheming } from '@/store/theme.ts'
|
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||||
import { GAME_MODES, HeadingLink, injectNotificationManager } from '@modrinth/ui'
|
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||||
|
import { watch, onMounted, onUnmounted, ref, computed } from 'vue'
|
||||||
import type { Dayjs } from 'dayjs'
|
import type { Dayjs } from 'dayjs'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
import { useTheming } from '@/store/theme.ts'
|
||||||
|
import { kill, run } from '@/helpers/profile'
|
||||||
const { handleError } = injectNotificationManager()
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { process_listener, profile_listener } from '@/helpers/events'
|
||||||
|
import { get_all } from '@/helpers/process'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import { handleSevereError } from '@/store/error'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
recentInstances: GameInstance[]
|
recentInstances: GameInstance[]
|
||||||
@@ -35,7 +33,7 @@ const theme = useTheming()
|
|||||||
|
|
||||||
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
const jumpBackInItems = ref<JumpBackInItem[]>([])
|
||||||
const serverData = ref<Record<string, ServerData>>({})
|
const serverData = ref<Record<string, ServerData>>({})
|
||||||
const protocolVersions = ref<Record<string, ProtocolVersion | null>>({})
|
const protocolVersions = ref<Record<string, number | null>>({})
|
||||||
|
|
||||||
const MIN_JUMP_BACK_IN = 3
|
const MIN_JUMP_BACK_IN = 3
|
||||||
const MAX_JUMP_BACK_IN = 6
|
const MAX_JUMP_BACK_IN = 6
|
||||||
@@ -123,8 +121,11 @@ async function populateJumpBackIn() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
servers.forEach(({ instancePath, address }) =>
|
// fetch each server's data
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
Promise.all(
|
||||||
|
servers.map(({ instancePath, address }) =>
|
||||||
|
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +150,8 @@ async function populateJumpBackIn() {
|
|||||||
.slice(0, MAX_JUMP_BACK_IN)
|
.slice(0, MAX_JUMP_BACK_IN)
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshServer(address: string, instancePath: string) {
|
async function refreshServer(address: string, instancePath: string) {
|
||||||
refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
await refreshServerData(serverData.value[address], protocolVersions.value[instancePath], address)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinWorld(world: WorldWithProfile) {
|
async function joinWorld(world: WorldWithProfile) {
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import type {
|
import type { ServerStatus, ServerWorld, SingleplayerWorld, World } from '@/helpers/worlds.ts'
|
||||||
ProtocolVersion,
|
|
||||||
ServerStatus,
|
|
||||||
ServerWorld,
|
|
||||||
SingleplayerWorld,
|
|
||||||
World,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
import { set_world_display_status, getWorldIdentifier } from '@/helpers/worlds.ts'
|
||||||
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
import { formatNumber, getPingLevel } from '@modrinth/utils'
|
||||||
import {
|
import {
|
||||||
@@ -60,9 +54,8 @@ const props = withDefaults(
|
|||||||
playingInstance?: boolean
|
playingInstance?: boolean
|
||||||
playingWorld?: boolean
|
playingWorld?: boolean
|
||||||
startingInstance?: boolean
|
startingInstance?: boolean
|
||||||
supportsServerQuickPlay?: boolean
|
supportsQuickPlay?: boolean
|
||||||
supportsWorldQuickPlay?: boolean
|
currentProtocol?: number | null
|
||||||
currentProtocol?: ProtocolVersion | null
|
|
||||||
highlighted?: boolean
|
highlighted?: boolean
|
||||||
|
|
||||||
// Server only
|
// Server only
|
||||||
@@ -85,8 +78,7 @@ const props = withDefaults(
|
|||||||
playingInstance: false,
|
playingInstance: false,
|
||||||
playingWorld: false,
|
playingWorld: false,
|
||||||
startingInstance: false,
|
startingInstance: false,
|
||||||
supportsServerQuickPlay: true,
|
supportsQuickPlay: false,
|
||||||
supportsWorldQuickPlay: false,
|
|
||||||
currentProtocol: null,
|
currentProtocol: null,
|
||||||
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
@@ -110,8 +102,7 @@ const serverIncompatible = computed(
|
|||||||
!!props.serverStatus &&
|
!!props.serverStatus &&
|
||||||
!!props.serverStatus.version?.protocol &&
|
!!props.serverStatus.version?.protocol &&
|
||||||
!!props.currentProtocol &&
|
!!props.currentProtocol &&
|
||||||
(props.serverStatus.version.protocol !== props.currentProtocol.version ||
|
props.serverStatus.version.protocol !== props.currentProtocol,
|
||||||
props.serverStatus.version.legacy !== props.currentProtocol.legacy),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
const locked = computed(() => props.world.type === 'singleplayer' && props.world.locked)
|
||||||
@@ -129,13 +120,9 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.a_minecraft_server',
|
id: 'instance.worlds.a_minecraft_server',
|
||||||
defaultMessage: 'A Minecraft Server',
|
defaultMessage: 'A Minecraft Server',
|
||||||
},
|
},
|
||||||
noServerQuickPlay: {
|
noQuickPlay: {
|
||||||
id: 'instance.worlds.no_server_quick_play',
|
id: 'instance.worlds.no_quick_play',
|
||||||
defaultMessage: 'You can only jump straight into servers on Minecraft Alpha 1.0.5+',
|
defaultMessage: 'You can only jump straight into worlds on Minecraft 1.20+',
|
||||||
},
|
|
||||||
noSingleplayerQuickPlay: {
|
|
||||||
id: 'instance.worlds.no_singleplayer_quick_play',
|
|
||||||
defaultMessage: 'You can only jump straight into singleplayer worlds on Minecraft 1.20+',
|
|
||||||
},
|
},
|
||||||
gameAlreadyOpen: {
|
gameAlreadyOpen: {
|
||||||
id: 'instance.worlds.game_already_open',
|
id: 'instance.worlds.game_already_open',
|
||||||
@@ -157,6 +144,10 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.view_instance',
|
id: 'instance.worlds.view_instance',
|
||||||
defaultMessage: 'View instance',
|
defaultMessage: 'View instance',
|
||||||
},
|
},
|
||||||
|
playAnyway: {
|
||||||
|
id: 'instance.worlds.play_anyway',
|
||||||
|
defaultMessage: 'Play anyway',
|
||||||
|
},
|
||||||
playInstance: {
|
playInstance: {
|
||||||
id: 'instance.worlds.play_instance',
|
id: 'instance.worlds.play_instance',
|
||||||
defaultMessage: 'Play instance',
|
defaultMessage: 'Play instance',
|
||||||
@@ -331,24 +322,17 @@ const messages = defineMessages({
|
|||||||
<ButtonStyled v-else>
|
<ButtonStyled v-else>
|
||||||
<button
|
<button
|
||||||
v-tooltip="
|
v-tooltip="
|
||||||
world.type == 'server' && !supportsServerQuickPlay
|
!serverStatus
|
||||||
? formatMessage(messages.noServerQuickPlay)
|
? formatMessage(messages.noContact)
|
||||||
: world.type == 'singleplayer' && !supportsWorldQuickPlay
|
: serverIncompatible
|
||||||
? formatMessage(messages.noSingleplayerQuickPlay)
|
? formatMessage(messages.incompatibleServer)
|
||||||
: playingOtherWorld || locked
|
: !supportsQuickPlay
|
||||||
? formatMessage(messages.gameAlreadyOpen)
|
? formatMessage(messages.noQuickPlay)
|
||||||
: !serverStatus
|
: playingOtherWorld || locked
|
||||||
? formatMessage(messages.noContact)
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
: serverIncompatible
|
: null
|
||||||
? formatMessage(messages.incompatibleServer)
|
|
||||||
: null
|
|
||||||
"
|
|
||||||
:disabled="
|
|
||||||
playingOtherWorld ||
|
|
||||||
startingInstance ||
|
|
||||||
(world.type == 'server' && !supportsServerQuickPlay) ||
|
|
||||||
(world.type == 'singleplayer' && !supportsWorldQuickPlay)
|
|
||||||
"
|
"
|
||||||
|
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
||||||
@click="emit('play')"
|
@click="emit('play')"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||||
@@ -365,6 +349,11 @@ const messages = defineMessages({
|
|||||||
disabled: playingInstance,
|
disabled: playingInstance,
|
||||||
action: () => emit('play-instance'),
|
action: () => emit('play-instance'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'play-anyway',
|
||||||
|
shown: serverIncompatible && !playingInstance && supportsQuickPlay,
|
||||||
|
action: () => emit('play'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'open-instance',
|
id: 'open-instance',
|
||||||
shown: !!instancePath,
|
shown: !!instancePath,
|
||||||
@@ -430,6 +419,10 @@ const messages = defineMessages({
|
|||||||
<PlayIcon aria-hidden="true" />
|
<PlayIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.playInstance) }}
|
{{ formatMessage(messages.playInstance) }}
|
||||||
</template>
|
</template>
|
||||||
|
<template #play-anyway>
|
||||||
|
<PlayIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.playAnyway) }}
|
||||||
|
</template>
|
||||||
<template #open-instance>
|
<template #open-instance>
|
||||||
<EyeIcon aria-hidden="true" />
|
<EyeIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.viewInstance) }}
|
{{ formatMessage(messages.viewInstance) }}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
|
||||||
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
import { PlayIcon, PlusIcon, XIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import InstanceModalTitlePrefix from '@/components/ui/modal/InstanceModalTitlePrefix.vue'
|
||||||
|
import { add_server_to_profile, type ServerPackStatus, type ServerWorld } from '@/helpers/worlds.ts'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import {
|
import {
|
||||||
|
type ServerPackStatus,
|
||||||
edit_server_in_profile,
|
edit_server_in_profile,
|
||||||
|
type ServerWorld,
|
||||||
set_world_display_status,
|
set_world_display_status,
|
||||||
type DisplayStatus,
|
type DisplayStatus,
|
||||||
type ServerPackStatus,
|
|
||||||
type ServerWorld,
|
|
||||||
} from '@/helpers/worlds.ts'
|
} from '@/helpers/worlds.ts'
|
||||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import { ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref } from 'vue'
|
import { handleError } from '@/store/notifications'
|
||||||
|
import ServerModalBody from '@/components/ui/world/modal/ServerModalBody.vue'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ChevronRightIcon, SaveIcon, XIcon, UndoIcon } from '@modrinth/assets'
|
||||||
|
import { Avatar, ButtonStyled, commonMessages } from '@modrinth/ui'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
import type { GameInstance } from '@/helpers/types'
|
||||||
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
import type { DisplayStatus, SingleplayerWorld } from '@/helpers/worlds.ts'
|
||||||
import { rename_world, reset_world_icon, set_world_display_status } from '@/helpers/worlds.ts'
|
import { set_world_display_status, rename_world, reset_world_icon } from '@/helpers/worlds.ts'
|
||||||
import { ChevronRightIcon, SaveIcon, UndoIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import { Avatar, ButtonStyled, commonMessages, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import { computed, ref } from 'vue'
|
import { handleError } from '@/store/notifications'
|
||||||
|
import HideFromHomeOption from '@/components/ui/world/modal/HideFromHomeOption.vue'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
import { get_max_memory } from '@/helpers/jre.js'
|
import { get_max_memory } from '@/helpers/jre.js'
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||||
|
|
||||||
const snapPoints = computed(() => {
|
const snapPoints = computed(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
|
||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
import { handleError } from '@/store/state.js'
|
||||||
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
|
|
||||||
export const useFetch = async (url, item, isSilent) => {
|
export const useFetch = async (url, item, isSilent) => {
|
||||||
try {
|
try {
|
||||||
@@ -11,7 +11,6 @@ export const useFetch = async (url, item, isSilent) => {
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isSilent) {
|
if (!isSilent) {
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
handleError({ message: `Error fetching ${item}` })
|
handleError({ message: `Error fetching ${item}` })
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|||||||
@@ -16,7 +16,3 @@ export async function logout() {
|
|||||||
export async function get() {
|
export async function get() {
|
||||||
return await invoke('plugin:mr-auth|get')
|
return await invoke('plugin:mr-auth|get')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelLogin() {
|
|
||||||
return await invoke('plugin:mr-auth|cancel_modrinth_login')
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||||
* and deserialized into a usable JS object.
|
* and deserialized into a usable JS object.
|
||||||
*/
|
*/
|
||||||
import { install_to_existing_profile } from '@/helpers/pack.js'
|
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { install_to_existing_profile } from '@/helpers/pack.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
|
||||||
/// Add instance
|
/// Add instance
|
||||||
/*
|
/*
|
||||||
@@ -190,7 +190,6 @@ export async function edit_icon(path, iconPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function finish_install(instance) {
|
export async function finish_install(instance) {
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
if (instance.install_stage !== 'pack_installed') {
|
if (instance.install_stage !== 'pack_installed') {
|
||||||
let linkedData = instance.linked_data
|
let linkedData = instance.linked_data
|
||||||
await install_to_existing_profile(
|
await install_to_existing_profile(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||||
|
|
||||||
export interface Cape {
|
export interface Cape {
|
||||||
id: string
|
id: string
|
||||||
@@ -39,7 +39,6 @@ export const DEFAULT_MODELS: Record<string, SkinModel> = {
|
|||||||
|
|
||||||
export function filterSavedSkins(list: Skin[]) {
|
export function filterSavedSkins(list: Skin[]) {
|
||||||
const customSkins = list.filter((s) => s.source !== 'default')
|
const customSkins = list.filter((s) => s.source !== 'default')
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
fixUnknownSkins(customSkins).catch(handleError)
|
fixUnknownSkins(customSkins).catch(handleError)
|
||||||
return customSkins
|
return customSkins
|
||||||
}
|
}
|
||||||
@@ -68,8 +67,9 @@ export async function determineModelType(texture: string): Promise<'SLIM' | 'CLA
|
|||||||
const armWidth = 2
|
const armWidth = 2
|
||||||
const armHeight = 12
|
const armHeight = 12
|
||||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||||
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
|
for (let index = 1; index <= imageData.length; index++) {
|
||||||
if (imageData[alphaIndex] !== 0) {
|
//every fourth value in RGBA is the alpha channel
|
||||||
|
if (index % 4 == 0 && imageData[index - 1] !== 0) {
|
||||||
resolve('CLASSIC')
|
resolve('CLASSIC')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export type ServerStatus = {
|
|||||||
version?: {
|
version?: {
|
||||||
name: string
|
name: string
|
||||||
protocol: number
|
protocol: number
|
||||||
legacy: boolean
|
|
||||||
}
|
}
|
||||||
favicon?: string
|
favicon?: string
|
||||||
enforces_secure_chat: boolean
|
enforces_secure_chat: boolean
|
||||||
@@ -71,17 +70,11 @@ export interface Chat {
|
|||||||
|
|
||||||
export type ServerData = {
|
export type ServerData = {
|
||||||
refreshing: boolean
|
refreshing: boolean
|
||||||
lastSuccessfulRefresh?: number
|
|
||||||
status?: ServerStatus
|
status?: ServerStatus
|
||||||
rawMotd?: string | Chat
|
rawMotd?: string | Chat
|
||||||
renderedMotd?: string
|
renderedMotd?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProtocolVersion = {
|
|
||||||
version: number
|
|
||||||
legacy: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get_recent_worlds(
|
export async function get_recent_worlds(
|
||||||
limit: number,
|
limit: number,
|
||||||
displayStatuses?: DisplayStatus[],
|
displayStatuses?: DisplayStatus[],
|
||||||
@@ -163,13 +156,13 @@ export async function remove_server_from_profile(path: string, index: number): P
|
|||||||
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
return await invoke('plugin:worlds|remove_server_from_profile', { path, index })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_profile_protocol_version(path: string): Promise<ProtocolVersion | null> {
|
export async function get_profile_protocol_version(path: string): Promise<number | null> {
|
||||||
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
return await invoke('plugin:worlds|get_profile_protocol_version', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_server_status(
|
export async function get_server_status(
|
||||||
address: string,
|
address: string,
|
||||||
protocolVersion: ProtocolVersion | null = null,
|
protocolVersion: number | null = null,
|
||||||
): Promise<ServerStatus> {
|
): Promise<ServerStatus> {
|
||||||
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
return await invoke('plugin:worlds|get_server_status', { address, protocolVersion })
|
||||||
}
|
}
|
||||||
@@ -213,39 +206,30 @@ export function isServerWorld(world: World): world is ServerWorld {
|
|||||||
|
|
||||||
export async function refreshServerData(
|
export async function refreshServerData(
|
||||||
serverData: ServerData,
|
serverData: ServerData,
|
||||||
protocolVersion: ProtocolVersion | null,
|
protocolVersion: number | null,
|
||||||
address: string,
|
address: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const refreshTime = Date.now()
|
|
||||||
serverData.refreshing = true
|
serverData.refreshing = true
|
||||||
await get_server_status(address, protocolVersion)
|
await get_server_status(address, protocolVersion)
|
||||||
.then((status) => {
|
.then((status) => {
|
||||||
if (serverData.lastSuccessfulRefresh && serverData.lastSuccessfulRefresh > refreshTime) {
|
|
||||||
// Don't update if there was a more recent successful refresh
|
|
||||||
return
|
|
||||||
}
|
|
||||||
serverData.lastSuccessfulRefresh = Date.now()
|
|
||||||
serverData.status = status
|
serverData.status = status
|
||||||
if (status.description) {
|
if (status.description) {
|
||||||
serverData.rawMotd = status.description
|
serverData.rawMotd = status.description
|
||||||
serverData.renderedMotd = autoToHTML(status.description)
|
serverData.renderedMotd = autoToHTML(status.description)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`Refreshing addr: ${address}`, err)
|
||||||
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
serverData.refreshing = false
|
serverData.refreshing = false
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
|
||||||
console.error(`Refreshing addr ${address}`, protocolVersion, err)
|
|
||||||
if (!protocolVersion?.legacy) {
|
|
||||||
refreshServerData(serverData, { version: 74, legacy: true }, address)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshServers(
|
export async function refreshServers(
|
||||||
worlds: World[],
|
worlds: World[],
|
||||||
serverData: Record<string, ServerData>,
|
serverData: Record<string, ServerData>,
|
||||||
protocolVersion: ProtocolVersion | null,
|
protocolVersion: number | null,
|
||||||
) {
|
) {
|
||||||
const servers = worlds.filter(isServerWorld)
|
const servers = worlds.filter(isServerWorld)
|
||||||
servers.forEach((server) => {
|
servers.forEach((server) => {
|
||||||
@@ -259,8 +243,10 @@ export function refreshServers(
|
|||||||
})
|
})
|
||||||
|
|
||||||
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
// noinspection ES6MissingAwait - handled with .then by refreshServerData already
|
||||||
Object.keys(serverData).forEach((address) =>
|
Promise.all(
|
||||||
refreshServerData(serverData[address], protocolVersion, address),
|
Object.keys(serverData).map((address) =>
|
||||||
|
refreshServerData(serverData[address], protocolVersion, address),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,24 +297,15 @@ export async function refreshWorlds(instancePath: string): Promise<World[]> {
|
|||||||
return worlds ?? []
|
return worlds ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasServerQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
const FIRST_QUICK_PLAY_VERSION = '23w14a'
|
||||||
if (!gameVersions.length) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
export function hasQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
||||||
const targetIndex = gameVersions.findIndex((v) => v.version === 'a1.0.5_01')
|
|
||||||
|
|
||||||
return versionIndex === -1 || targetIndex === -1 || versionIndex <= targetIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVersion: string) {
|
|
||||||
if (!gameVersions.length) {
|
if (!gameVersions.length) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
const versionIndex = gameVersions.findIndex((v) => v.version === currentVersion)
|
||||||
const targetIndex = gameVersions.findIndex((v) => v.version === '23w14a')
|
const targetIndex = gameVersions.findIndex((v) => v.version === FIRST_QUICK_PLAY_VERSION)
|
||||||
|
|
||||||
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,11 +383,11 @@
|
|||||||
"instance.worlds.no_contact": {
|
"instance.worlds.no_contact": {
|
||||||
"message": "Server couldn't be contacted"
|
"message": "Server couldn't be contacted"
|
||||||
},
|
},
|
||||||
"instance.worlds.no_server_quick_play": {
|
"instance.worlds.no_quick_play": {
|
||||||
"message": "You can only jump straight into servers on Minecraft Alpha 1.0.5+"
|
"message": "You can only jump straight into worlds on Minecraft 1.20+"
|
||||||
},
|
},
|
||||||
"instance.worlds.no_singleplayer_quick_play": {
|
"instance.worlds.play_anyway": {
|
||||||
"message": "You can only jump straight into singleplayer worlds on Minecraft 1.20+"
|
"message": "Play anyway"
|
||||||
},
|
},
|
||||||
"instance.worlds.play_instance": {
|
"instance.worlds.play_instance": {
|
||||||
"message": "Play instance"
|
"message": "Play instance"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import App from '@/App.vue'
|
import { createApp } from 'vue'
|
||||||
import router from '@/routes'
|
import router from '@/routes'
|
||||||
import * as Sentry from '@sentry/vue'
|
import App from '@/App.vue'
|
||||||
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
import { createPinia } from 'pinia'
|
||||||
import { createPlugin } from '@vintl/vintl/plugin'
|
|
||||||
import FloatingVue from 'floating-vue'
|
import FloatingVue from 'floating-vue'
|
||||||
import 'floating-vue/dist/style.css'
|
import 'floating-vue/dist/style.css'
|
||||||
import { createPinia } from 'pinia'
|
import { createPlugin } from '@vintl/vintl/plugin'
|
||||||
import { createApp } from 'vue'
|
import * as Sentry from '@sentry/vue'
|
||||||
|
import { VueScanPlugin } from '@taijased/vue-render-tracker'
|
||||||
|
|
||||||
const VIntlPlugin = createPlugin({
|
const VIntlPlugin = createPlugin({
|
||||||
controllerOpts: {
|
controllerOpts: {
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||||
import type Instance from '@/components/ui/Instance.vue'
|
import type { Ref } from 'vue'
|
||||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
import { SearchIcon, XIcon, ClipboardCopyIcon, GlobeIcon, ExternalIcon } from '@modrinth/assets'
|
||||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
|
||||||
import SearchCard from '@/components/ui/SearchCard.vue'
|
|
||||||
import { get_search_results } from '@/helpers/cache.js'
|
|
||||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
|
|
||||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import { ClipboardCopyIcon, ExternalIcon, GlobeIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
import type { Category, GameVersion, Platform, ProjectType, SortType, Tags } from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
|
SearchFilterControl,
|
||||||
|
SearchSidebarFilter,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
DropdownSelect,
|
DropdownSelect,
|
||||||
injectNotificationManager,
|
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
Pagination,
|
Pagination,
|
||||||
SearchFilterControl,
|
|
||||||
SearchSidebarFilter,
|
|
||||||
useSearch,
|
useSearch,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
import { handleError } from '@/store/state'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
import type { Ref } from 'vue'
|
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
|
||||||
import type { LocationQuery } from 'vue-router'
|
import type { LocationQuery } from 'vue-router'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import SearchCard from '@/components/ui/SearchCard.vue'
|
||||||
|
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile.js'
|
||||||
|
import { get_search_results } from '@/helpers/cache.js'
|
||||||
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
|
import type Instance from '@/components/ui/Instance.vue'
|
||||||
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -221,7 +220,7 @@ async function refreshSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.value = rawResults.result
|
results.value = rawResults.result
|
||||||
currentPage.value = 1
|
currentPage.value = Math.max(1, Math.min(pageCount.value, currentPage.value))
|
||||||
|
|
||||||
const persistentParams: LocationQuery = {}
|
const persistentParams: LocationQuery = {}
|
||||||
|
|
||||||
@@ -267,7 +266,6 @@ async function onSearchChangeToTop() {
|
|||||||
|
|
||||||
function clearSearch() {
|
function clearSearch() {
|
||||||
query.value = ''
|
query.value = ''
|
||||||
currentPage.value = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import RowDisplay from '@/components/RowDisplay.vue'
|
import { ref, onUnmounted, computed } from 'vue'
|
||||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
|
||||||
import { get_search_results } from '@/helpers/cache.js'
|
|
||||||
import { profile_listener } from '@/helpers/events'
|
|
||||||
import { list } from '@/helpers/profile.js'
|
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import type { SearchResult } from '@modrinth/utils'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { computed, onUnmounted, ref } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import RowDisplay from '@/components/RowDisplay.vue'
|
||||||
|
import { list } from '@/helpers/profile.js'
|
||||||
|
import { profile_listener } from '@/helpers/events'
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { get_search_results } from '@/helpers/cache.js'
|
||||||
|
import type { SearchResult } from '@modrinth/utils'
|
||||||
|
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
|
||||||
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
|
|
||||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
|
||||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
|
||||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
|
||||||
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
|
|
||||||
import { get as getSettings } from '@/helpers/settings.ts'
|
|
||||||
import type { Cape, Skin } from '@/helpers/skins.ts'
|
|
||||||
import {
|
|
||||||
equip_skin,
|
|
||||||
filterDefaultSkins,
|
|
||||||
filterSavedSkins,
|
|
||||||
get_available_capes,
|
|
||||||
get_available_skins,
|
|
||||||
get_normalized_skin_texture,
|
|
||||||
normalize_skin_texture,
|
|
||||||
remove_custom_skin,
|
|
||||||
set_default_cape,
|
|
||||||
} from '@/helpers/skins.ts'
|
|
||||||
import { handleSevereError } from '@/store/error'
|
|
||||||
import {
|
import {
|
||||||
EditIcon,
|
EditIcon,
|
||||||
ExcitedRinthbot,
|
ExcitedRinthbot,
|
||||||
@@ -34,21 +12,42 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
injectNotificationManager,
|
|
||||||
SkinButton,
|
SkinButton,
|
||||||
SkinLikeTextButton,
|
SkinLikeTextButton,
|
||||||
SkinPreviewRenderer,
|
SkinPreviewRenderer,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
|
||||||
import { computedAsync } from '@vueuse/core'
|
import { computedAsync } from '@vueuse/core'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
|
||||||
|
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||||
|
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||||
|
import { handleError, useNotifications } from '@/store/notifications'
|
||||||
|
import type { Cape, Skin } from '@/helpers/skins.ts'
|
||||||
|
import {
|
||||||
|
normalize_skin_texture,
|
||||||
|
equip_skin,
|
||||||
|
filterDefaultSkins,
|
||||||
|
filterSavedSkins,
|
||||||
|
get_available_capes,
|
||||||
|
get_available_skins,
|
||||||
|
get_normalized_skin_texture,
|
||||||
|
remove_custom_skin,
|
||||||
|
set_default_cape,
|
||||||
|
} from '@/helpers/skins.ts'
|
||||||
|
import { get as getSettings } from '@/helpers/settings.ts'
|
||||||
|
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||||
|
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||||
|
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||||
|
import { handleSevereError } from '@/store/error'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||||
|
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||||
const editSkinModal = useTemplateRef('editSkinModal')
|
const editSkinModal = useTemplateRef('editSkinModal')
|
||||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||||
const uploadSkinModal = useTemplateRef('uploadSkinModal')
|
const uploadSkinModal = useTemplateRef('uploadSkinModal')
|
||||||
|
|
||||||
const notifications = injectNotificationManager()
|
const notifications = useNotifications()
|
||||||
const { handleError } = notifications
|
|
||||||
|
|
||||||
const settings = ref(await getSettings())
|
const settings = ref(await getSettings())
|
||||||
const skins = ref<Skin[]>([])
|
const skins = ref<Skin[]>([])
|
||||||
@@ -114,7 +113,7 @@ async function loadCapes() {
|
|||||||
defaultCape.value = capes.value.find((c) => c.is_equipped)
|
defaultCape.value = capes.value.find((c) => c.is_equipped)
|
||||||
originalDefaultCape.value = defaultCape.value
|
originalDefaultCape.value = defaultCape.value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentUser.value && error instanceof Error) {
|
if (currentUser.value) {
|
||||||
handleError(error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,7 +126,7 @@ async function loadSkins() {
|
|||||||
selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
|
selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
|
||||||
originalSelectedSkin.value = selectedSkin.value
|
originalSelectedSkin.value = selectedSkin.value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentUser.value && error instanceof Error) {
|
if (currentUser.value) {
|
||||||
handleError(error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,7 +161,7 @@ async function changeSkin(newSkin: Skin) {
|
|||||||
text: "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
text: "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
handleError(error as Error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,7 +190,7 @@ async function handleCapeSelected(cape: Cape | undefined) {
|
|||||||
text: "You're changing your cape too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
text: "You're changing your cape too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.",
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
handleError(error as Error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,7 +207,7 @@ async function loadCurrentUser() {
|
|||||||
const allAccounts = await users()
|
const allAccounts = await users()
|
||||||
currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId)
|
currentUser.value = allAccounts.find((acc) => acc.profile.id === defaultId)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e as Error)
|
handleError(e)
|
||||||
currentUser.value = undefined
|
currentUser.value = undefined
|
||||||
currentUserId.value = undefined
|
currentUserId.value = undefined
|
||||||
}
|
}
|
||||||
@@ -277,7 +276,7 @@ async function checkUserChanges() {
|
|||||||
await loadSkins()
|
await loadSkins()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (currentUser.value && error instanceof Error) {
|
if (currentUser.value) {
|
||||||
handleError(error)
|
handleError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,7 +376,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
|
|||||||
color="green"
|
color="green"
|
||||||
aria-label="Edit skin"
|
aria-label="Edit skin"
|
||||||
class="pointer-events-auto"
|
class="pointer-events-auto"
|
||||||
@click.stop="(e: MouseEvent) => editSkinModal?.show(e, skin)"
|
@click.stop="(e) => editSkinModal?.show(e, skin)"
|
||||||
>
|
>
|
||||||
<EditIcon /> Edit
|
<EditIcon /> Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -157,18 +157,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
import {
|
||||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
Avatar,
|
||||||
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
ButtonStyled,
|
||||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
ContentPageHeader,
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
LoadingIndicator,
|
||||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
OverflowMenu,
|
||||||
import { process_listener, profile_listener } from '@/helpers/events'
|
} from '@modrinth/ui'
|
||||||
import { get_by_profile_path } from '@/helpers/process'
|
|
||||||
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
|
||||||
import { showProfileInFolder } from '@/helpers/utils.js'
|
|
||||||
import { handleSevereError } from '@/store/error.js'
|
|
||||||
import { useBreadcrumbs, useLoading } from '@/store/state'
|
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
@@ -192,25 +187,28 @@ import {
|
|||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
|
||||||
Avatar,
|
import { get_by_profile_path } from '@/helpers/process'
|
||||||
ButtonStyled,
|
import { process_listener, profile_listener } from '@/helpers/events'
|
||||||
ContentPageHeader,
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
injectNotificationManager,
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
LoadingIndicator,
|
import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
|
||||||
OverflowMenu,
|
import { showProfileInFolder } from '@/helpers/utils.js'
|
||||||
} from '@modrinth/ui'
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||||
|
import { handleSevereError } from '@/store/error.js'
|
||||||
|
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import duration from 'dayjs/plugin/duration'
|
import duration from 'dayjs/plugin/duration'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import InstanceSettingsModal from '@/components/ui/modal/InstanceSettingsModal.vue'
|
||||||
|
|
||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -88,30 +88,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
|
||||||
import { process_listener } from '@/helpers/events.js'
|
import { Button, Card, Checkbox, DropdownSelect } from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
delete_logs_by_filename,
|
delete_logs_by_filename,
|
||||||
get_latest_log_cursor,
|
|
||||||
get_logs,
|
get_logs,
|
||||||
get_output_by_filename,
|
get_output_by_filename,
|
||||||
|
get_latest_log_cursor,
|
||||||
} from '@/helpers/logs.js'
|
} from '@/helpers/logs.js'
|
||||||
import { get_by_profile_path } from '@/helpers/process.js'
|
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { CheckIcon, ClipboardCopyIcon, ShareIcon, TrashIcon } from '@modrinth/assets'
|
|
||||||
import { Button, Card, Checkbox, DropdownSelect, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import isToday from 'dayjs/plugin/isToday'
|
import isToday from 'dayjs/plugin/isToday'
|
||||||
import isYesterday from 'dayjs/plugin/isYesterday'
|
import isYesterday from 'dayjs/plugin/isYesterday'
|
||||||
import { ofetch } from 'ofetch'
|
import { get_by_profile_path } from '@/helpers/process.js'
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { process_listener } from '@/helpers/events.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { ofetch } from 'ofetch'
|
||||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||||
|
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||||
|
|
||||||
dayjs.extend(isToday)
|
dayjs.extend(isToday)
|
||||||
dayjs.extend(isYesterday)
|
dayjs.extend(isYesterday)
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -249,30 +249,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { TextInputIcon } from '@/assets/icons'
|
|
||||||
import AddContentButton from '@/components/ui/AddContentButton.vue'
|
|
||||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
|
||||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
|
||||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
|
||||||
import {
|
|
||||||
get_organization_many,
|
|
||||||
get_project_many,
|
|
||||||
get_team_many,
|
|
||||||
get_version_many,
|
|
||||||
} from '@/helpers/cache.js'
|
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
|
||||||
import {
|
|
||||||
add_project_from_path,
|
|
||||||
get_projects,
|
|
||||||
remove_project,
|
|
||||||
toggle_disable_project,
|
|
||||||
update_all,
|
|
||||||
update_project,
|
|
||||||
} from '@/helpers/profile.js'
|
|
||||||
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
|
|
||||||
import { highlightModInProfile } from '@/helpers/utils.js'
|
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ClipboardCopyIcon,
|
ClipboardCopyIcon,
|
||||||
@@ -295,22 +271,44 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
ContentListPanel,
|
ContentListPanel,
|
||||||
injectNotificationManager,
|
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
Pagination,
|
Pagination,
|
||||||
RadialHeader,
|
RadialHeader,
|
||||||
Toggle,
|
Toggle,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
|
|
||||||
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
|
import type { Organization, Project, TeamMember, Version } from '@modrinth/utils'
|
||||||
import { formatProjectType } from '@modrinth/utils'
|
import { formatProjectType } from '@modrinth/utils'
|
||||||
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef } from 'vue'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||||
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
const { handleError } = injectNotificationManager()
|
import {
|
||||||
|
add_project_from_path,
|
||||||
|
get_projects,
|
||||||
|
remove_project,
|
||||||
|
toggle_disable_project,
|
||||||
|
update_all,
|
||||||
|
update_project,
|
||||||
|
} from '@/helpers/profile.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
|
import { highlightModInProfile } from '@/helpers/utils.js'
|
||||||
|
import { TextInputIcon } from '@/assets/icons'
|
||||||
|
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||||
|
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||||
|
import AddContentButton from '@/components/ui/AddContentButton.vue'
|
||||||
|
import {
|
||||||
|
get_organization_many,
|
||||||
|
get_project_many,
|
||||||
|
get_team_many,
|
||||||
|
get_version_many,
|
||||||
|
} from '@/helpers/cache.js'
|
||||||
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
|
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||||
|
import { getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import type { CacheBehaviour, ContentFile, GameInstance } from '@/helpers/types'
|
||||||
|
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import type { ContentItem } from '@modrinth/ui/src/components/content/ContentListItem.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance: GameInstance
|
instance: GameInstance
|
||||||
|
|||||||
@@ -67,8 +67,7 @@
|
|||||||
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
:key="`world-${world.type}-${world.type == 'singleplayer' ? world.path : `${world.address}-${world.index}`}`"
|
||||||
:world="world"
|
:world="world"
|
||||||
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
:highlighted="highlightedWorld === getWorldIdentifier(world)"
|
||||||
:supports-server-quick-play="supportsServerQuickPlay"
|
:supports-quick-play="supportsQuickPlay"
|
||||||
:supports-world-quick-play="supportsWorldQuickPlay"
|
|
||||||
:current-protocol="protocolVersion"
|
:current-protocol="protocolVersion"
|
||||||
:playing-instance="playing"
|
:playing-instance="playing"
|
||||||
:playing-world="worldsMatch(world, worldPlaying)"
|
:playing-world="worldsMatch(world, worldPlaying)"
|
||||||
@@ -121,55 +120,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
import { ref, computed, onUnmounted, watch } from 'vue'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import { useRoute } from 'vue-router'
|
||||||
|
import type { GameInstance } from '@/helpers/types'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonStyled,
|
||||||
|
RadialHeader,
|
||||||
|
FilterBar,
|
||||||
|
type FilterBarOption,
|
||||||
|
type GameVersion,
|
||||||
|
GAME_MODES,
|
||||||
|
} from '@modrinth/ui'
|
||||||
|
import { PlusIcon, SpinnerIcon, UpdatedIcon, SearchIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import {
|
||||||
|
type SingleplayerWorld,
|
||||||
|
type World,
|
||||||
|
type ServerWorld,
|
||||||
|
type ServerData,
|
||||||
|
type ProfileEvent,
|
||||||
|
get_profile_protocol_version,
|
||||||
|
remove_server_from_profile,
|
||||||
|
delete_world,
|
||||||
|
start_join_server,
|
||||||
|
start_join_singleplayer_world,
|
||||||
|
getWorldIdentifier,
|
||||||
|
refreshServerData,
|
||||||
|
refreshWorld,
|
||||||
|
sortWorlds,
|
||||||
|
refreshServers,
|
||||||
|
hasQuickPlaySupport,
|
||||||
|
refreshWorlds,
|
||||||
|
handleDefaultProfileUpdateEvent,
|
||||||
|
showWorldInFolder,
|
||||||
|
} from '@/helpers/worlds.ts'
|
||||||
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
import AddServerModal from '@/components/ui/world/modal/AddServerModal.vue'
|
||||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||||
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
|
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
|
||||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||||
|
|
||||||
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import { handleError } from '@/store/notifications'
|
||||||
|
import type ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import type { Version } from '@modrinth/utils'
|
||||||
import { profile_listener } from '@/helpers/events'
|
import { profile_listener } from '@/helpers/events'
|
||||||
import { get_game_versions } from '@/helpers/tags'
|
import { get_game_versions } from '@/helpers/tags'
|
||||||
import type { GameInstance } from '@/helpers/types'
|
|
||||||
import {
|
|
||||||
type ProfileEvent,
|
|
||||||
type ProtocolVersion,
|
|
||||||
type ServerData,
|
|
||||||
type ServerWorld,
|
|
||||||
type SingleplayerWorld,
|
|
||||||
type World,
|
|
||||||
delete_world,
|
|
||||||
getWorldIdentifier,
|
|
||||||
get_profile_protocol_version,
|
|
||||||
handleDefaultProfileUpdateEvent,
|
|
||||||
hasServerQuickPlaySupport,
|
|
||||||
hasWorldQuickPlaySupport,
|
|
||||||
refreshServerData,
|
|
||||||
refreshServers,
|
|
||||||
refreshWorld,
|
|
||||||
refreshWorlds,
|
|
||||||
remove_server_from_profile,
|
|
||||||
showWorldInFolder,
|
|
||||||
sortWorlds,
|
|
||||||
start_join_server,
|
|
||||||
start_join_singleplayer_world,
|
|
||||||
} from '@/helpers/worlds.ts'
|
|
||||||
import { PlusIcon, SearchIcon, SpinnerIcon, UpdatedIcon, XIcon } from '@modrinth/assets'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonStyled,
|
|
||||||
FilterBar,
|
|
||||||
type FilterBarOption,
|
|
||||||
GAME_MODES,
|
|
||||||
type GameVersion,
|
|
||||||
RadialHeader,
|
|
||||||
injectNotificationManager,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import type { Version } from '@modrinth/utils'
|
|
||||||
import { defineMessages } from '@vintl/vintl'
|
import { defineMessages } from '@vintl/vintl'
|
||||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const addServerModal = ref<InstanceType<typeof AddServerModal>>()
|
const addServerModal = ref<InstanceType<typeof AddServerModal>>()
|
||||||
@@ -213,9 +210,7 @@ const worldPlaying = ref<World>()
|
|||||||
const worlds = ref<World[]>([])
|
const worlds = ref<World[]>([])
|
||||||
const serverData = ref<Record<string, ServerData>>({})
|
const serverData = ref<Record<string, ServerData>>({})
|
||||||
|
|
||||||
const protocolVersion = ref<ProtocolVersion | null>(
|
const protocolVersion = ref<number | null>(await get_profile_protocol_version(instance.value.path))
|
||||||
await get_profile_protocol_version(instance.value.path),
|
|
||||||
)
|
|
||||||
|
|
||||||
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
const unlistenProfile = await profile_listener(async (e: ProfileEvent) => {
|
||||||
if (e.profile_path_id !== instance.value.path) return
|
if (e.profile_path_id !== instance.value.path) return
|
||||||
@@ -251,7 +246,7 @@ async function refreshAllWorlds() {
|
|||||||
worlds.value = await refreshWorlds(instance.value.path).finally(
|
worlds.value = await refreshWorlds(instance.value.path).finally(
|
||||||
() => (refreshingAll.value = false),
|
() => (refreshingAll.value = false),
|
||||||
)
|
)
|
||||||
refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
await refreshServers(worlds.value, serverData.value, protocolVersion.value)
|
||||||
|
|
||||||
const hasNoWorlds = worlds.value.length === 0
|
const hasNoWorlds = worlds.value.length === 0
|
||||||
|
|
||||||
@@ -282,7 +277,7 @@ async function editServer(server: ServerWorld) {
|
|||||||
await refreshServer(server.address)
|
await refreshServer(server.address)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handleError(new Error(`Error refreshing server, refreshing all worlds`))
|
handleError(`Error refreshing server, refreshing all worlds`)
|
||||||
await refreshAllWorlds()
|
await refreshAllWorlds()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +296,7 @@ async function editWorld(path: string, name: string, removeIcon: boolean) {
|
|||||||
}
|
}
|
||||||
sortWorlds(worlds.value)
|
sortWorlds(worlds.value)
|
||||||
} else {
|
} else {
|
||||||
handleError(new Error(`Error finding world in list, refreshing all worlds`))
|
handleError(`Error finding world in list, refreshing all worlds`)
|
||||||
await refreshAllWorlds()
|
await refreshAllWorlds()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,7 +306,7 @@ async function deleteWorld(world: SingleplayerWorld) {
|
|||||||
worlds.value = worlds.value.filter((w) => w.type !== 'singleplayer' || w.path !== world.path)
|
worlds.value = worlds.value.filter((w) => w.type !== 'singleplayer' || w.path !== world.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleJoinError(err: Error) {
|
function handleJoinError(err: unknown) {
|
||||||
handleError(err)
|
handleError(err)
|
||||||
startingInstance.value = false
|
startingInstance.value = false
|
||||||
worldPlaying.value = undefined
|
worldPlaying.value = undefined
|
||||||
@@ -357,11 +352,8 @@ function worldsMatch(world: World, other: World | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []))
|
const gameVersions = ref<GameVersion[]>(await get_game_versions().catch(() => []))
|
||||||
const supportsServerQuickPlay = computed(() =>
|
const supportsQuickPlay = computed(() =>
|
||||||
hasServerQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
hasQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
||||||
)
|
|
||||||
const supportsWorldQuickPlay = computed(() =>
|
|
||||||
hasWorldQuickPlaySupport(gameVersions.value, instance.value.game_version),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const filterOptions = computed(() => {
|
const filterOptions = computed(() => {
|
||||||
@@ -436,7 +428,7 @@ function promptToRemoveWorld(world: World): boolean {
|
|||||||
|
|
||||||
async function proceedRemoveServer() {
|
async function proceedRemoveServer() {
|
||||||
if (!serverToRemove.value) {
|
if (!serverToRemove.value) {
|
||||||
handleError(new Error(`Error removing server, no server marked for removal.`))
|
handleError(`Error removing server, no server marked for removal.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await removeServer(serverToRemove.value)
|
await removeServer(serverToRemove.value)
|
||||||
@@ -445,7 +437,7 @@ async function proceedRemoveServer() {
|
|||||||
|
|
||||||
async function proceedDeleteWorld() {
|
async function proceedDeleteWorld() {
|
||||||
if (!worldToDelete.value) {
|
if (!worldToDelete.value) {
|
||||||
handleError(new Error(`Error deleting world, no world marked for removal.`))
|
handleError(`Error deleting world, no world marked for removal.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await deleteWorld(worldToDelete.value)
|
await deleteWorld(worldToDelete.value)
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { NewInstanceImage } from '@/assets/icons'
|
|
||||||
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
|
||||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
|
||||||
import { profile_listener } from '@/helpers/events.js'
|
|
||||||
import { list } from '@/helpers/profile.js'
|
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
|
|
||||||
import { PlusIcon } from '@modrinth/assets'
|
|
||||||
import { Button, injectNotificationManager } from '@modrinth/ui'
|
|
||||||
import { onUnmounted, ref, shallowRef } from 'vue'
|
import { onUnmounted, ref, shallowRef } from 'vue'
|
||||||
|
import { list } from '@/helpers/profile.js'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs.js'
|
||||||
|
import { profile_listener } from '@/helpers/events.js'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { Button } from '@modrinth/ui'
|
||||||
|
import { PlusIcon } from '@modrinth/assets'
|
||||||
|
import InstanceCreationModal from '@/components/ui/InstanceCreationModal.vue'
|
||||||
|
import { NewInstanceImage } from '@/assets/icons'
|
||||||
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
|
|
||||||
|
|||||||
@@ -129,46 +129,46 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
|
||||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
|
||||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
|
||||||
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
|
||||||
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
|
||||||
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
|
||||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
|
||||||
import { install as installVersion } from '@/store/install.js'
|
|
||||||
import { useTheming } from '@/store/state.js'
|
|
||||||
import {
|
import {
|
||||||
BookmarkIcon,
|
BookmarkIcon,
|
||||||
CheckIcon,
|
|
||||||
ClipboardCopyIcon,
|
|
||||||
DownloadIcon,
|
|
||||||
ExternalIcon,
|
|
||||||
GlobeIcon,
|
|
||||||
HeartIcon,
|
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
|
DownloadIcon,
|
||||||
ReportIcon,
|
ReportIcon,
|
||||||
|
HeartIcon,
|
||||||
|
ExternalIcon,
|
||||||
|
CheckIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
ClipboardCopyIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
ButtonStyled,
|
|
||||||
OverflowMenu,
|
|
||||||
ProjectBackgroundGradient,
|
|
||||||
ProjectHeader,
|
ProjectHeader,
|
||||||
ProjectSidebarCompatibility,
|
ProjectSidebarCompatibility,
|
||||||
|
ButtonStyled,
|
||||||
|
OverflowMenu,
|
||||||
|
ProjectSidebarLinks,
|
||||||
ProjectSidebarCreators,
|
ProjectSidebarCreators,
|
||||||
ProjectSidebarDetails,
|
ProjectSidebarDetails,
|
||||||
ProjectSidebarLinks,
|
ProjectBackgroundGradient,
|
||||||
injectNotificationManager,
|
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
|
import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||||
|
import { get as getInstance, get_projects as getInstanceProjects } from '@/helpers/profile'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { ref, shallowRef, watch } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ref, shallowRef, watch } from 'vue'
|
||||||
|
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||||
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||||
|
import { install as installVersion } from '@/store/install.js'
|
||||||
|
import { get_project, get_team, get_version_many } from '@/helpers/cache.js'
|
||||||
|
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||||
|
import { useTheming } from '@/store/state.js'
|
||||||
|
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const breadcrumbs = useBreadcrumbs()
|
const breadcrumbs = useBreadcrumbs()
|
||||||
|
|||||||
@@ -65,16 +65,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ProjectPageVersions, ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||||
|
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||||
|
import { ref } from 'vue'
|
||||||
import { SwapIcon } from '@/assets/icons/index.js'
|
import { SwapIcon } from '@/assets/icons/index.js'
|
||||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||||
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import {
|
|
||||||
ButtonStyled,
|
|
||||||
OverflowMenu,
|
|
||||||
ProjectPageVersions,
|
|
||||||
injectNotificationManager,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
project: {
|
project: {
|
||||||
@@ -107,8 +103,6 @@ defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
|
|
||||||
const [loaders, gameVersions] = await Promise.all([
|
const [loaders, gameVersions] = await Promise.all([
|
||||||
get_loaders().catch(handleError).then(ref),
|
get_loaders().catch(handleError).then(ref),
|
||||||
get_game_versions().catch(handleError).then(ref),
|
get_game_versions().catch(handleError).then(ref),
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import {
|
|
||||||
AbstractWebNotificationManager,
|
|
||||||
type NotificationPanelLocation,
|
|
||||||
type WebNotification,
|
|
||||||
} from '@modrinth/ui'
|
|
||||||
import { ref, type Ref } from 'vue'
|
|
||||||
|
|
||||||
export class AppNotificationManager extends AbstractWebNotificationManager {
|
|
||||||
private readonly state: Ref<WebNotification[]>
|
|
||||||
private readonly locationState: Ref<NotificationPanelLocation>
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
super()
|
|
||||||
this.state = ref<WebNotification[]>([])
|
|
||||||
this.locationState = ref<NotificationPanelLocation>('right')
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNotificationLocation(): NotificationPanelLocation {
|
|
||||||
return this.locationState.value
|
|
||||||
}
|
|
||||||
|
|
||||||
public setNotificationLocation(location: NotificationPanelLocation): void {
|
|
||||||
this.locationState.value = location
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNotifications(): WebNotification[] {
|
|
||||||
return this.state.value
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addNotificationToStorage(notification: WebNotification): void {
|
|
||||||
this.state.value.push(notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected removeNotificationFromStorage(id: string | number): void {
|
|
||||||
const index = this.state.value.findIndex((n) => n.id === id)
|
|
||||||
if (index > -1) {
|
|
||||||
this.state.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected removeNotificationFromStorageByIndex(index: number): void {
|
|
||||||
this.state.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected clearAllNotificationsFromStorage(): void {
|
|
||||||
this.state.value.splice(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { trackEvent } from '@/helpers/analytics.js'
|
import { defineStore } from 'pinia'
|
||||||
import { get_project, get_version_many } from '@/helpers/cache.js'
|
|
||||||
import { create_profile_and_install as packInstall } from '@/helpers/pack.js'
|
|
||||||
import {
|
import {
|
||||||
add_project_from_version,
|
add_project_from_version,
|
||||||
check_installed,
|
check_installed,
|
||||||
@@ -9,9 +7,11 @@ import {
|
|||||||
list,
|
list,
|
||||||
remove_project,
|
remove_project,
|
||||||
} from '@/helpers/profile.js'
|
} from '@/helpers/profile.js'
|
||||||
import { injectNotificationManager } from '@modrinth/ui'
|
import { handleError } from '@/store/notifications.js'
|
||||||
|
import { get_project, get_version_many } from '@/helpers/cache.js'
|
||||||
|
import { create_profile_and_install as packInstall } from '@/helpers/pack.js'
|
||||||
|
import { trackEvent } from '@/helpers/analytics.js'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
|
|
||||||
export const useInstall = defineStore('installStore', {
|
export const useInstall = defineStore('installStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -49,7 +49,6 @@ export const install = async (
|
|||||||
callback = () => {},
|
callback = () => {},
|
||||||
createInstanceCallback = () => {},
|
createInstanceCallback = () => {},
|
||||||
) => {
|
) => {
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
const project = await get_project(projectId, 'must_revalidate').catch(handleError)
|
const project = await get_project(projectId, 'must_revalidate').catch(handleError)
|
||||||
|
|
||||||
if (project.project_type === 'modpack') {
|
if (project.project_type === 'modpack') {
|
||||||
@@ -161,7 +160,6 @@ export const install = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const installVersionDependencies = async (profile, version) => {
|
export const installVersionDependencies = async (profile, version) => {
|
||||||
const { handleError } = injectNotificationManager()
|
|
||||||
for (const dep of version.dependencies) {
|
for (const dep of version.dependencies) {
|
||||||
if (dep.dependency_type !== 'required') continue
|
if (dep.dependency_type !== 'required') continue
|
||||||
// disallow fabric api install on quilt
|
// disallow fabric api install on quilt
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useNotifications = defineStore('notificationsStore', {
|
||||||
|
state: () => ({
|
||||||
|
notificationsWrapper: null,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
setNotifs(notifs) {
|
||||||
|
this.notificationsWrapper = notifs
|
||||||
|
},
|
||||||
|
addNotification(notif) {
|
||||||
|
this.notificationsWrapper.addNotification(notif)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const handleError = (err) => {
|
||||||
|
const notifs = useNotifications()
|
||||||
|
notifs.addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: err.message ?? err,
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useBreadcrumbs } from './breadcrumbs'
|
|
||||||
import { useInstall } from './install'
|
|
||||||
import { useLoading } from './loading'
|
|
||||||
import { useTheming } from './theme.ts'
|
import { useTheming } from './theme.ts'
|
||||||
|
import { useBreadcrumbs } from './breadcrumbs'
|
||||||
|
import { useLoading } from './loading'
|
||||||
|
import { useNotifications, handleError } from './notifications'
|
||||||
|
import { useInstall } from './install'
|
||||||
|
|
||||||
export { useBreadcrumbs, useInstall, useLoading, useTheming }
|
export { useTheming, useBreadcrumbs, useLoading, useNotifications, handleError, useInstall }
|
||||||
@@ -15,7 +15,7 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
|||||||
println!("A browser window will now open, follow the login flow there.");
|
println!("A browser window will now open, follow the login flow there.");
|
||||||
let login = minecraft_auth::begin_login().await?;
|
let login = minecraft_auth::begin_login().await?;
|
||||||
|
|
||||||
println!("Open URL {} in a browser", login.auth_request_uri.as_str());
|
println!("Open URL {} in a browser", login.redirect_uri.as_str());
|
||||||
|
|
||||||
println!("Please enter URL code: ");
|
println!("Please enter URL code: ");
|
||||||
let mut input = String::new();
|
let mut input = String::new();
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ thiserror.workspace = true
|
|||||||
daedalus.workspace = true
|
daedalus.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
either.workspace = true
|
either.workspace = true
|
||||||
hyper = { workspace = true, features = ["server"] }
|
|
||||||
hyper-util.workspace = true
|
|
||||||
|
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
urlencoding.workspace = true
|
urlencoding.workspace = true
|
||||||
|
|||||||
+1
-6
@@ -120,12 +120,7 @@ fn main() {
|
|||||||
.plugin(
|
.plugin(
|
||||||
"mr-auth",
|
"mr-auth",
|
||||||
InlinedPlugin::new()
|
InlinedPlugin::new()
|
||||||
.commands(&[
|
.commands(&["modrinth_login", "logout", "get"])
|
||||||
"modrinth_login",
|
|
||||||
"logout",
|
|
||||||
"get",
|
|
||||||
"cancel_modrinth_login",
|
|
||||||
])
|
|
||||||
.default_permission(
|
.default_permission(
|
||||||
DefaultPermissionRule::AllowAllCommands,
|
DefaultPermissionRule::AllowAllCommands,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ pub async fn login<R: Runtime>(
|
|||||||
let window = tauri::WebviewWindowBuilder::new(
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
&app,
|
&app,
|
||||||
"signin",
|
"signin",
|
||||||
tauri::WebviewUrl::External(flow.auth_request_uri.parse().map_err(
|
tauri::WebviewUrl::External(flow.redirect_uri.parse().map_err(
|
||||||
|_| {
|
|_| {
|
||||||
theseus::ErrorKind::OtherError(
|
theseus::ErrorKind::OtherError(
|
||||||
"Error parsing auth redirect URL".to_string(),
|
"Error parsing auth redirect URL".to_string(),
|
||||||
@@ -77,7 +77,6 @@ pub async fn login<R: Runtime>(
|
|||||||
window.close()?;
|
window.close()?;
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
|
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
|
||||||
Ok(minecraft_auth::remove_user(user).await?)
|
Ok(minecraft_auth::remove_user(user).await?)
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ pub mod cache;
|
|||||||
pub mod friends;
|
pub mod friends;
|
||||||
pub mod worlds;
|
pub mod worlds;
|
||||||
|
|
||||||
mod oauth_utils;
|
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
pub type Result<T> = std::result::Result<T, TheseusSerializableError>;
|
||||||
|
|
||||||
// // Main returnable Theseus GUI error
|
// // Main returnable Theseus GUI error
|
||||||
|
|||||||
+60
-56
@@ -1,70 +1,79 @@
|
|||||||
use crate::api::Result;
|
use crate::api::Result;
|
||||||
use crate::api::TheseusSerializableError;
|
use chrono::{Duration, Utc};
|
||||||
use crate::api::oauth_utils;
|
|
||||||
use tauri::Manager;
|
|
||||||
use tauri::Runtime;
|
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri::{Manager, Runtime, UserAttentionType};
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
use tokio::sync::oneshot;
|
|
||||||
|
|
||||||
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||||
tauri::plugin::Builder::new("mr-auth")
|
tauri::plugin::Builder::new("mr-auth")
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![modrinth_login, logout, get,])
|
||||||
modrinth_login,
|
|
||||||
logout,
|
|
||||||
get,
|
|
||||||
cancel_modrinth_login,
|
|
||||||
])
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn modrinth_login<R: Runtime>(
|
pub async fn modrinth_login<R: Runtime>(
|
||||||
app: tauri::AppHandle<R>,
|
app: tauri::AppHandle<R>,
|
||||||
) -> Result<ModrinthCredentials> {
|
) -> Result<Option<ModrinthCredentials>> {
|
||||||
let (auth_code_recv_socket_tx, auth_code_recv_socket) = oneshot::channel();
|
let redirect_uri = mr_auth::authenticate_begin_flow();
|
||||||
let auth_code = tokio::spawn(oauth_utils::auth_code_reply::listen(
|
|
||||||
auth_code_recv_socket_tx,
|
|
||||||
));
|
|
||||||
|
|
||||||
let auth_code_recv_socket = auth_code_recv_socket.await.unwrap()?;
|
let start = Utc::now();
|
||||||
|
|
||||||
let auth_request_uri = format!(
|
if let Some(window) = app.get_webview_window("modrinth-signin") {
|
||||||
"{}?launcher=true&ipver={}&port={}",
|
window.close()?;
|
||||||
mr_auth::authenticate_begin_flow(),
|
|
||||||
if auth_code_recv_socket.is_ipv4() {
|
|
||||||
"4"
|
|
||||||
} else {
|
|
||||||
"6"
|
|
||||||
},
|
|
||||||
auth_code_recv_socket.port()
|
|
||||||
);
|
|
||||||
|
|
||||||
app.opener()
|
|
||||||
.open_url(auth_request_uri, None::<&str>)
|
|
||||||
.map_err(|e| {
|
|
||||||
TheseusSerializableError::Theseus(
|
|
||||||
theseus::ErrorKind::OtherError(format!(
|
|
||||||
"Failed to open auth request URI: {e}"
|
|
||||||
))
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let Some(auth_code) = auth_code.await.unwrap()? else {
|
|
||||||
return Err(TheseusSerializableError::Theseus(
|
|
||||||
theseus::ErrorKind::OtherError("Login canceled".into()).into(),
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
let credentials = mr_auth::authenticate_finish_flow(&auth_code).await?;
|
|
||||||
|
|
||||||
if let Some(main_window) = app.get_window("main") {
|
|
||||||
main_window.set_focus().ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(credentials)
|
let window = tauri::WebviewWindowBuilder::new(
|
||||||
|
&app,
|
||||||
|
"modrinth-signin",
|
||||||
|
tauri::WebviewUrl::External(redirect_uri.parse().map_err(|_| {
|
||||||
|
theseus::ErrorKind::OtherError(
|
||||||
|
"Error parsing auth redirect URL".to_string(),
|
||||||
|
)
|
||||||
|
.as_error()
|
||||||
|
})?),
|
||||||
|
)
|
||||||
|
.min_inner_size(420.0, 632.0)
|
||||||
|
.inner_size(420.0, 632.0)
|
||||||
|
.max_inner_size(420.0, 632.0)
|
||||||
|
.zoom_hotkeys_enabled(false)
|
||||||
|
.title("Sign into Modrinth")
|
||||||
|
.always_on_top(true)
|
||||||
|
.center()
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
window.request_user_attention(Some(UserAttentionType::Critical))?;
|
||||||
|
|
||||||
|
while (Utc::now() - start) < Duration::minutes(10) {
|
||||||
|
if window.title().is_err() {
|
||||||
|
// user closed window, cancelling flow
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if window
|
||||||
|
.url()?
|
||||||
|
.as_str()
|
||||||
|
.starts_with("https://launcher-files.modrinth.com")
|
||||||
|
{
|
||||||
|
let url = window.url()?;
|
||||||
|
|
||||||
|
let code = url.query_pairs().find(|(key, _)| key == "code");
|
||||||
|
|
||||||
|
window.close()?;
|
||||||
|
|
||||||
|
return if let Some((_, code)) = code {
|
||||||
|
let val = mr_auth::authenticate_finish_flow(&code).await?;
|
||||||
|
|
||||||
|
Ok(Some(val))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.close()?;
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -76,8 +85,3 @@ pub async fn logout() -> Result<()> {
|
|||||||
pub async fn get() -> Result<Option<ModrinthCredentials>> {
|
pub async fn get() -> Result<Option<ModrinthCredentials>> {
|
||||||
Ok(theseus::mr_auth::get_credentials().await?)
|
Ok(theseus::mr_auth::get_credentials().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub fn cancel_modrinth_login() {
|
|
||||||
oauth_utils::auth_code_reply::stop_listeners();
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
//! A minimal OAuth 2.0 authorization code grant flow redirection/reply loopback URI HTTP
|
|
||||||
//! server implementation, compliant with [RFC 6749]'s authorization code grant flow and
|
|
||||||
//! [RFC 8252]'s best current practices for OAuth 2.0 in native apps.
|
|
||||||
//!
|
|
||||||
//! This server is needed for the step 4 of the OAuth authentication dance represented in
|
|
||||||
//! figure 1 of [RFC 8252].
|
|
||||||
//!
|
|
||||||
//! Further reading: https://www.oauth.com/oauth2-servers/oauth-native-apps/redirect-urls-for-native-apps/
|
|
||||||
//!
|
|
||||||
//! [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
|
||||||
//! [RFC 8252]: https://datatracker.ietf.org/doc/html/rfc8252
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
|
||||||
sync::{LazyLock, Mutex},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use hyper::body::Incoming;
|
|
||||||
use hyper_util::rt::{TokioIo, TokioTimer};
|
|
||||||
use theseus::ErrorKind;
|
|
||||||
use tokio::{
|
|
||||||
net::TcpListener,
|
|
||||||
sync::{broadcast, oneshot},
|
|
||||||
};
|
|
||||||
|
|
||||||
static SERVER_SHUTDOWN: LazyLock<broadcast::Sender<()>> =
|
|
||||||
LazyLock::new(|| broadcast::channel(1024).0);
|
|
||||||
|
|
||||||
/// Starts a temporary HTTP server to receive OAuth 2.0 authorization code grant flow redirects
|
|
||||||
/// on a loopback interface with an ephemeral port. The caller can know the bound socket address
|
|
||||||
/// by listening on the counterpart channel for `listen_socket_tx`.
|
|
||||||
///
|
|
||||||
/// If the server is stopped before receiving an authorization code, `Ok(None)` is returned.
|
|
||||||
pub async fn listen(
|
|
||||||
listen_socket_tx: oneshot::Sender<Result<SocketAddr, theseus::Error>>,
|
|
||||||
) -> Result<Option<String>, theseus::Error> {
|
|
||||||
// IPv4 is tried first for the best compatibility and performance with most systems.
|
|
||||||
// IPv6 is also tried in case IPv4 is not available. Resolving "localhost" is avoided
|
|
||||||
// to prevent failures deriving from improper name resolution setup. Any available
|
|
||||||
// ephemeral port is used to prevent conflicts with other services. This is all as per
|
|
||||||
// RFC 8252's recommendations
|
|
||||||
const ANY_LOOPBACK_SOCKET: &[SocketAddr] = &[
|
|
||||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0),
|
|
||||||
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0),
|
|
||||||
];
|
|
||||||
|
|
||||||
let listener = match TcpListener::bind(ANY_LOOPBACK_SOCKET).await {
|
|
||||||
Ok(listener) => {
|
|
||||||
listen_socket_tx
|
|
||||||
.send(listener.local_addr().map_err(|e| {
|
|
||||||
ErrorKind::OtherError(format!(
|
|
||||||
"Failed to get auth code reply socket address: {e}"
|
|
||||||
))
|
|
||||||
.into()
|
|
||||||
}))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
listener
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let error_msg =
|
|
||||||
format!("Failed to bind auth code reply socket: {e}");
|
|
||||||
|
|
||||||
listen_socket_tx
|
|
||||||
.send(Err(ErrorKind::OtherError(error_msg.clone()).into()))
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
return Err(ErrorKind::OtherError(error_msg).into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut auth_code = Mutex::new(None);
|
|
||||||
let mut shutdown_notification = SERVER_SHUTDOWN.subscribe();
|
|
||||||
|
|
||||||
while auth_code.get_mut().unwrap().is_none() {
|
|
||||||
let client_socket = tokio::select! {
|
|
||||||
biased;
|
|
||||||
_ = shutdown_notification.recv() => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
conn_accept_result = listener.accept() => {
|
|
||||||
match conn_accept_result {
|
|
||||||
Ok((socket, _)) => socket,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Failed to accept auth code reply: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = hyper::server::conn::http1::Builder::new()
|
|
||||||
.keep_alive(false)
|
|
||||||
.header_read_timeout(Duration::from_secs(5))
|
|
||||||
.timer(TokioTimer::new())
|
|
||||||
.auto_date_header(false)
|
|
||||||
.serve_connection(
|
|
||||||
TokioIo::new(client_socket),
|
|
||||||
hyper::service::service_fn(|req| handle_reply(req, &auth_code)),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!("Failed to handle auth code reply: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(auth_code.into_inner().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops any active OAuth 2.0 authorization code grant flow reply listening HTTP servers.
|
|
||||||
pub fn stop_listeners() {
|
|
||||||
SERVER_SHUTDOWN.send(()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_reply(
|
|
||||||
req: hyper::Request<Incoming>,
|
|
||||||
auth_code_out: &Mutex<Option<String>>,
|
|
||||||
) -> Result<hyper::Response<String>, hyper::http::Error> {
|
|
||||||
if req.method() != hyper::Method::GET {
|
|
||||||
return hyper::Response::builder()
|
|
||||||
.status(hyper::StatusCode::METHOD_NOT_ALLOWED)
|
|
||||||
.header("Allow", "GET")
|
|
||||||
.body("".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// The authorization code is guaranteed to be sent as a "code" query parameter
|
|
||||||
// in the request URI query string as per RFC 6749 § 4.1.2
|
|
||||||
let auth_code = req.uri().query().and_then(|query_string| {
|
|
||||||
query_string
|
|
||||||
.split('&')
|
|
||||||
.filter_map(|query_pair| query_pair.split_once('='))
|
|
||||||
.find_map(|(key, value)| (key == "code").then_some(value))
|
|
||||||
});
|
|
||||||
|
|
||||||
let response = if let Some(auth_code) = auth_code {
|
|
||||||
*auth_code_out.lock().unwrap() = Some(auth_code.to_string());
|
|
||||||
|
|
||||||
hyper::Response::builder()
|
|
||||||
.status(hyper::StatusCode::OK)
|
|
||||||
.header("Content-Type", "text/html;charset=utf-8")
|
|
||||||
.body(
|
|
||||||
include_str!("auth_code_reply/page.html")
|
|
||||||
.replace("{{title}}", "Success")
|
|
||||||
.replace("{{message}}", "You have successfully signed in! You can close this page now."),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
hyper::Response::builder()
|
|
||||||
.status(hyper::StatusCode::BAD_REQUEST)
|
|
||||||
.header("Content-Type", "text/html;charset=utf-8")
|
|
||||||
.body(
|
|
||||||
include_str!("auth_code_reply/page.html")
|
|
||||||
.replace("{{title}}", "Error")
|
|
||||||
.replace("{{message}}", "Authorization code not found. Please try signing in again."),
|
|
||||||
)
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!doctype html><html class=dark-mode lang=en><meta content="width=device-width,initial-scale=1" name=viewport><title>Sign In - Modrinth App</title><style>*,:after,:before,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scroll-snap-strictness:proximity;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.\!container{width:100%!important}.container{width:100%}@media (width>=640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (width>=768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (width>=1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (width>=1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (width>=1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.\!fixed{position:fixed!important}.fixed{position:fixed}.absolute{position:absolute}.\!relative{position:relative!important}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-\[calc\(-1rem-2px\)_-2rem\]{inset:calc(-1rem - 2px) -2rem}.-right-12{right:-3rem}.-top-12{top:-3rem}.-top-28{top:-7rem}.bottom-0{bottom:0}.bottom-1{bottom:.25rem}.bottom-\[10\%\]{bottom:10%}.bottom-\[18\%\]{bottom:18%}.bottom-\[250px\]{bottom:250px}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-2{right:.5rem}.right-8{right:2rem}.top-0{top:0}.top-0\.5{top:.125rem}.top-2{top:.5rem}.top-\[-4rem\]{top:-4rem}.top-\[18\%\]{top:18%}.top-\[3px\]{top:3px}.isolate{isolation:isolate}.-z-10{z-index:-10}.z-10{z-index:10}.z-50{z-index:50}.z-\[1\]{z-index:1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-full{grid-column:1/-1}.col-start-1{grid-column-start:1}.row-start-1{grid-row-start:1}.float-end{float:inline-end}.m-0{margin:0}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-6{margin:1.5rem}.\!mx-2{margin-left:.5rem!important;margin-right:.5rem!important}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-auto{margin-top:auto;margin-bottom:auto}.mb-0{margin-bottom:0}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.ml-0{margin-left:0}.ml-0\.5{margin-left:.125rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-8{margin-left:2rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-3{margin-right:.75rem}.mt-0{margin-top:0}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-9{margin-top:2.25rem}.mt-\[-1px\]{margin-top:-1px}.mt-auto{margin-top:auto}.mt-px{margin-top:1px}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\!block{display:block!important}.block{display:block}.inline-block{display:inline-block}.\!inline{display:inline!important}.inline{display:inline}.\!flex{display:flex!important}.flex{display:flex}.inline-flex{display:inline-flex}.\!table{display:table!important}.table{display:table}.inline-table{display:inline-table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row-group{display:table-row-group}.table-row{display:table-row}.\!grid{display:grid!important}.grid{display:grid}.contents{display:contents}.list-item{display:list-item}.\!hidden{display:none!important}.hidden{display:none}.aspect-\[10\/16\]{aspect-ratio:10/16}.aspect-\[16\/10\]{aspect-ratio:16/10}.aspect-\[2\/1\]{aspect-ratio:2}.aspect-\[5\/7\]{aspect-ratio:5/7}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:16/9}.size-1{width:.25rem;height:.25rem}.size-12{width:3rem;height:3rem}.size-3{width:.75rem;height:.75rem}.size-4{width:1rem;height:1rem}.size-48{width:12rem;height:12rem}.size-5{width:1.25rem;height:1.25rem}.siLine truncated
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
//! Assorted utilities for OAuth 2.0 authorization flows.
|
|
||||||
|
|
||||||
pub mod auth_code_reply;
|
|
||||||
@@ -250,7 +250,7 @@ pub async fn profile_get_pack_export_candidates(
|
|||||||
// invoke('plugin:profile|profile_run', path)
|
// invoke('plugin:profile|profile_run', path)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
pub async fn profile_run(path: &str) -> Result<ProcessMetadata> {
|
||||||
let process = profile::run(path, QuickPlayType::None).await?;
|
let process = profile::run(path, &QuickPlayType::None).await?;
|
||||||
|
|
||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ use enumset::EnumSet;
|
|||||||
use tauri::{AppHandle, Manager, Runtime};
|
use tauri::{AppHandle, Manager, Runtime};
|
||||||
use theseus::prelude::ProcessMetadata;
|
use theseus::prelude::ProcessMetadata;
|
||||||
use theseus::profile::{QuickPlayType, get_full_path};
|
use theseus::profile::{QuickPlayType, get_full_path};
|
||||||
use theseus::server_address::ServerAddress;
|
|
||||||
use theseus::worlds::{
|
use theseus::worlds::{
|
||||||
DisplayStatus, ProtocolVersion, ServerPackStatus, ServerStatus, World,
|
DisplayStatus, ServerPackStatus, ServerStatus, World, WorldType,
|
||||||
WorldType, WorldWithProfile,
|
WorldWithProfile,
|
||||||
};
|
};
|
||||||
use theseus::{profile, worlds};
|
use theseus::{profile, worlds};
|
||||||
|
|
||||||
@@ -184,16 +183,14 @@ pub async fn remove_server_from_profile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_profile_protocol_version(
|
pub async fn get_profile_protocol_version(path: &str) -> Result<Option<i32>> {
|
||||||
path: &str,
|
|
||||||
) -> Result<Option<ProtocolVersion>> {
|
|
||||||
Ok(worlds::get_profile_protocol_version(path).await?)
|
Ok(worlds::get_profile_protocol_version(path).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_server_status(
|
pub async fn get_server_status(
|
||||||
address: &str,
|
address: &str,
|
||||||
protocol_version: Option<ProtocolVersion>,
|
protocol_version: Option<i32>,
|
||||||
) -> Result<ServerStatus> {
|
) -> Result<ServerStatus> {
|
||||||
Ok(worlds::get_server_status(address, protocol_version).await?)
|
Ok(worlds::get_server_status(address, protocol_version).await?)
|
||||||
}
|
}
|
||||||
@@ -204,7 +201,7 @@ pub async fn start_join_singleplayer_world(
|
|||||||
world: String,
|
world: String,
|
||||||
) -> Result<ProcessMetadata> {
|
) -> Result<ProcessMetadata> {
|
||||||
let process =
|
let process =
|
||||||
profile::run(path, QuickPlayType::Singleplayer(world)).await?;
|
profile::run(path, &QuickPlayType::Singleplayer(world)).await?;
|
||||||
|
|
||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
@@ -214,11 +211,8 @@ pub async fn start_join_server(
|
|||||||
path: &str,
|
path: &str,
|
||||||
address: &str,
|
address: &str,
|
||||||
) -> Result<ProcessMetadata> {
|
) -> Result<ProcessMetadata> {
|
||||||
let process = profile::run(
|
let process =
|
||||||
path,
|
profile::run(path, &QuickPlayType::Server(address.to_owned())).await?;
|
||||||
QuickPlayType::Server(ServerAddress::Unresolved(address.to_owned())),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(process)
|
Ok(process)
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,6 @@
|
|||||||
"height": 800,
|
"height": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Modrinth App",
|
"title": "Modrinth App",
|
||||||
"label": "main",
|
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"minHeight": 700,
|
"minHeight": 700,
|
||||||
"minWidth": 1100,
|
"minWidth": 1100,
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM rust:1.88.0 AS build
|
FROM rust:1.88.0 AS build
|
||||||
|
|
||||||
WORKDIR /usr/src/daedalus
|
WORKDIR /usr/src/daedalus
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN --mount=type=cache,target=/usr/src/daedalus/target \
|
RUN cargo build --release --package daedalus_client
|
||||||
--mount=type=cache,target=/usr/local/cargo/git/db \
|
|
||||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
|
||||||
cargo build --release --package daedalus_client
|
|
||||||
|
|
||||||
FROM build AS artifacts
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/usr/src/daedalus/target \
|
|
||||||
mkdir /daedalus \
|
|
||||||
&& cp /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@@ -21,7 +11,7 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=artifacts /daedalus /daedalus
|
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||||
|
|
||||||
WORKDIR /daedalus_client
|
WORKDIR /daedalus_client
|
||||||
CMD ["/daedalus/daedalus_client"]
|
|
||||||
|
CMD /daedalus/daedalus_client
|
||||||
@@ -59,12 +59,10 @@
|
|||||||
"markdown-it": "14.1.0",
|
"markdown-it": "14.1.0",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pinia-plugin-persistedstate": "^4.4.1",
|
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"qrcode.vue": "^3.4.0",
|
"qrcode.vue": "^3.4.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"three": "^0.172.0",
|
"three": "^0.172.0",
|
||||||
"vue-confetti-explosion": "^1.0.2",
|
|
||||||
"vue-multiselect": "3.0.0-alpha.2",
|
"vue-multiselect": "3.0.0-alpha.2",
|
||||||
"vue-typed-virtual-list": "^1.0.10",
|
"vue-typed-virtual-list": "^1.0.10",
|
||||||
"vue3-ace-editor": "^2.2.4",
|
"vue3-ace-editor": "^2.2.4",
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<ModrinthLoadingIndicator />
|
<ModrinthLoadingIndicator />
|
||||||
<NotificationPanel />
|
<Notifications />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { NotificationPanel, provideNotificationManager } from "@modrinth/ui";
|
|
||||||
import { FrontendNotificationManager } from "./providers/frontend-notifications.ts";
|
|
||||||
import ModrinthLoadingIndicator from "~/components/ui/modrinth-loading-indicator.ts";
|
import ModrinthLoadingIndicator from "~/components/ui/modrinth-loading-indicator.ts";
|
||||||
|
import Notifications from "~/components/ui/Notifications.vue";
|
||||||
provideNotificationManager(new FrontendNotificationManager());
|
|
||||||
</script>
|
</script>
|
||||||
@@ -51,9 +51,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { PlusIcon, XIcon } from "@modrinth/assets";
|
||||||
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { injectNotificationManager } from "@modrinth/ui";
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
const router = useNativeRouter();
|
const router = useNativeRouter();
|
||||||
|
|
||||||
const name = ref("");
|
const name = ref("");
|
||||||
@@ -89,6 +87,7 @@ async function create() {
|
|||||||
await router.push(`/collection/${result.id}`);
|
await router.push(`/collection/${result.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addNotification({
|
addNotification({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: err?.data?.description || err?.message || err,
|
text: err?.data?.description || err?.message || err,
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -84,11 +84,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { NewModal, ButtonStyled, DropdownSelect } from "@modrinth/ui";
|
||||||
import { ButtonStyled, DropdownSelect, NewModal } from "@modrinth/ui";
|
import { XIcon, PlusIcon } from "@modrinth/assets";
|
||||||
import { injectNotificationManager } from "@modrinth/ui";
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const app = useNuxtApp();
|
const app = useNuxtApp();
|
||||||
@@ -183,7 +180,8 @@ async function createProject() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addNotification({
|
app.$notify({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,28 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { MailIcon, CheckIcon } from "@modrinth/assets";
|
import { MailIcon, CheckIcon } from "@modrinth/assets";
|
||||||
import { ref } from "vue";
|
import { ref, watchEffect } from "vue";
|
||||||
import { useBaseFetch } from "~/composables/fetch.js";
|
import { useBaseFetch } from "~/composables/fetch.js";
|
||||||
|
|
||||||
const auth = await useAuth();
|
const auth = await useAuth();
|
||||||
const showSubscriptionConfirmation = ref(false);
|
const showSubscriptionConfirmation = ref(false);
|
||||||
const showSubscribeButton = useAsyncData(
|
const subscribed = ref(false);
|
||||||
async () => {
|
|
||||||
if (auth.value?.user) {
|
async function checkSubscribed() {
|
||||||
try {
|
if (auth.value?.user) {
|
||||||
const { subscribed } = await useBaseFetch("auth/email/subscribe", {
|
try {
|
||||||
method: "GET",
|
const { data } = await useBaseFetch("auth/email/subscribe", {
|
||||||
});
|
method: "GET",
|
||||||
return !subscribed;
|
});
|
||||||
} catch {
|
subscribed.value = data?.subscribed || false;
|
||||||
return true;
|
} catch {
|
||||||
}
|
subscribed.value = false;
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{ watch: [auth], server: false },
|
}
|
||||||
);
|
|
||||||
|
watchEffect(() => {
|
||||||
|
checkSubscribed();
|
||||||
|
});
|
||||||
|
|
||||||
async function subscribe() {
|
async function subscribe() {
|
||||||
try {
|
try {
|
||||||
@@ -34,19 +35,14 @@ async function subscribe() {
|
|||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showSubscriptionConfirmation.value = false;
|
showSubscriptionConfirmation.value = false;
|
||||||
showSubscribeButton.status.value = "success";
|
subscribed.value = true;
|
||||||
showSubscribeButton.data.value = false;
|
|
||||||
}, 2500);
|
}, 2500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ButtonStyled
|
<ButtonStyled v-if="auth?.user && !subscribed" color="brand" type="outlined">
|
||||||
v-if="showSubscribeButton.status.value === 'success' && showSubscribeButton.data.value"
|
|
||||||
color="brand"
|
|
||||||
type="outlined"
|
|
||||||
>
|
|
||||||
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
|
<button v-tooltip="`Subscribe to the Modrinth newsletter`" @click="subscribe">
|
||||||
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
|
<template v-if="!showSubscriptionConfirmation"> <MailIcon /> Subscribe </template>
|
||||||
<template v-else> <CheckIcon /> Subscribed! </template>
|
<template v-else> <CheckIcon /> Subscribed! </template>
|
||||||
|
|||||||
@@ -319,21 +319,30 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { renderString } from "@modrinth/utils";
|
||||||
import {
|
import {
|
||||||
BellIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
CheckCircleIcon,
|
|
||||||
CheckIcon,
|
|
||||||
ExternalIcon,
|
|
||||||
ScaleIcon,
|
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
|
ScaleIcon,
|
||||||
|
BellIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
CalendarIcon,
|
||||||
VersionIcon,
|
VersionIcon,
|
||||||
|
CheckIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
ExternalIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { injectNotificationManager } from "@modrinth/ui";
|
import { Avatar, ProjectStatusBadge, CopyCode, useRelativeTime } from "@modrinth/ui";
|
||||||
|
import ThreadSummary from "~/components/ui/thread/ThreadSummary.vue";
|
||||||
|
import { getProjectLink, getVersionLink } from "~/helpers/projects.js";
|
||||||
|
import { getUserLink } from "~/helpers/users.js";
|
||||||
|
import { acceptTeamInvite, removeSelfFromTeam } from "~/helpers/teams.js";
|
||||||
|
import { markAsRead } from "~/helpers/notifications.ts";
|
||||||
|
import DoubleIcon from "~/components/ui/DoubleIcon.vue";
|
||||||
|
import Categories from "~/components/ui/search/Categories.vue";
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager();
|
const app = useNuxtApp();
|
||||||
const emit = defineEmits(["update:notifications"]);
|
const emit = defineEmits(["update:notifications"]);
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
const formatRelativeTime = useRelativeTime();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -398,7 +407,8 @@ async function read() {
|
|||||||
const newNotifs = updateNotifs(props.notifications);
|
const newNotifs = updateNotifs(props.notifications);
|
||||||
emit("update:notifications", newNotifs);
|
emit("update:notifications", newNotifs);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addNotification({
|
app.$notify({
|
||||||
|
group: "main",
|
||||||
title: "Error marking notification as read",
|
title: "Error marking notification as read",
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -417,7 +427,8 @@ async function performAction(notification, actionIndex) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addNotification({
|
app.$notify({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
+62
-81
@@ -3,9 +3,7 @@
|
|||||||
class="vue-notification-group experimental-styles-within"
|
class="vue-notification-group experimental-styles-within"
|
||||||
:class="{
|
:class="{
|
||||||
'intercom-present': isIntercomPresent,
|
'intercom-present': isIntercomPresent,
|
||||||
'location-left': notificationLocation === 'left',
|
rightwards: moveNotificationsRight,
|
||||||
'location-right': notificationLocation === 'right',
|
|
||||||
'has-sidebar': hasSidebar,
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<transition-group name="notifs">
|
<transition-group name="notifs">
|
||||||
@@ -55,7 +53,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled circular size="small">
|
<ButtonStyled circular size="small">
|
||||||
<button v-tooltip="`Dismiss`" @click="dismissNotification(index)">
|
<button v-tooltip="`Dismiss`" @click="notifications.splice(index, 1)">
|
||||||
<XIcon />
|
<XIcon />
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -75,117 +73,106 @@
|
|||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
<script setup lang="ts">
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import {
|
import {
|
||||||
|
XCircleIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CopyIcon,
|
|
||||||
InfoIcon,
|
InfoIcon,
|
||||||
IssuesIcon,
|
IssuesIcon,
|
||||||
XCircleIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
CopyIcon,
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
} from "@modrinth/assets";
|
||||||
import { injectNotificationManager, type WebNotification } from '../../providers'
|
const notifications = useNotifications();
|
||||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
const { isVisible: moveNotificationsRight } = useNotificationRightwards();
|
||||||
|
|
||||||
const notificationManager = injectNotificationManager()
|
const isIntercomPresent = ref(false);
|
||||||
const notifications = computed<WebNotification[]>(() => notificationManager.getNotifications())
|
|
||||||
const notificationLocation = computed(() => notificationManager.getNotificationLocation())
|
|
||||||
|
|
||||||
const isIntercomPresent = ref<boolean>(false)
|
function stopTimer(notif) {
|
||||||
const copied = ref<Record<string, boolean>>({})
|
clearTimeout(notif.timer);
|
||||||
|
|
||||||
const stopTimer = (n: WebNotification) => notificationManager.stopNotificationTimer(n)
|
|
||||||
const setNotificationTimer = (n: WebNotification) => notificationManager.setNotificationTimer(n)
|
|
||||||
const dismissNotification = (n: number) => notificationManager.removeNotificationByIndex(n)
|
|
||||||
|
|
||||||
function createNotifText(notif: WebNotification): string {
|
|
||||||
return [notif.title, notif.text, notif.errorCode].filter(Boolean).join('\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkIntercomPresence(): void {
|
const copied = ref({});
|
||||||
isIntercomPresent.value = !!document.querySelector('.intercom-lightweight-app')
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyToClipboard(notif: WebNotification): void {
|
const createNotifText = (notif) => {
|
||||||
const text = createNotifText(notif)
|
let text = "";
|
||||||
|
if (notif.title) {
|
||||||
|
text += notif.title;
|
||||||
|
}
|
||||||
|
if (notif.text) {
|
||||||
|
if (text.length > 0) {
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
|
text += notif.text;
|
||||||
|
}
|
||||||
|
if (notif.errorCode) {
|
||||||
|
if (text.length > 0) {
|
||||||
|
text += "\n";
|
||||||
|
}
|
||||||
|
text += notif.errorCode;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
copied.value[text] = true
|
function checkIntercomPresence() {
|
||||||
navigator.clipboard.writeText(text)
|
isIntercomPresent.value = !!document.querySelector(".intercom-lightweight-app");
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const { [text]: _, ...rest } = copied.value
|
|
||||||
copied.value = rest
|
|
||||||
}, 2000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkIntercomPresence()
|
checkIntercomPresence();
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
checkIntercomPresence()
|
checkIntercomPresence();
|
||||||
})
|
});
|
||||||
|
|
||||||
observer.observe(document.body, {
|
observer.observe(document.body, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
observer.disconnect()
|
observer.disconnect();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
withDefaults(
|
function copyToClipboard(notif) {
|
||||||
defineProps<{
|
const text = createNotifText(notif);
|
||||||
hasSidebar?: boolean
|
|
||||||
}>(),
|
copied.value[text] = true;
|
||||||
{
|
navigator.clipboard.writeText(text);
|
||||||
hasSidebar: false,
|
setTimeout(() => {
|
||||||
},
|
delete copied.value[text];
|
||||||
)
|
}, 2000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.vue-notification-group {
|
.vue-notification-group {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
right: 1.5rem;
|
||||||
bottom: 1.5rem;
|
bottom: 1.5rem;
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
width: 450px;
|
width: 450px;
|
||||||
|
|
||||||
&.location-right {
|
|
||||||
right: 1.5rem;
|
|
||||||
|
|
||||||
&.has-sidebar {
|
|
||||||
right: 325px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.location-left {
|
|
||||||
left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
width: calc(100% - 0.75rem * 2);
|
width: calc(100% - 0.75rem * 2);
|
||||||
|
right: 0.75rem;
|
||||||
bottom: 0.75rem;
|
bottom: 0.75rem;
|
||||||
|
|
||||||
&.location-right {
|
|
||||||
right: 0.75rem;
|
|
||||||
left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.location-left {
|
|
||||||
left: 0.75rem;
|
|
||||||
right: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.intercom-present {
|
&.intercom-present {
|
||||||
bottom: 5rem;
|
bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.rightwards {
|
||||||
|
right: unset !important;
|
||||||
|
left: 1.5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
left: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.vue-notification-wrapper {
|
.vue-notification-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -221,12 +208,6 @@ withDefaults(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notifs-leave-to {
|
.notifs-leave-to {
|
||||||
.location-right & {
|
transform: translateX(100%) scale(0.8);
|
||||||
transform: translateX(100%) scale(0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-left & {
|
|
||||||
transform: translateX(-100%) scale(0.8);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
maxlength="64"
|
maxlength="64"
|
||||||
:placeholder="`Enter organization name...`"
|
:placeholder="`Enter organization name...`"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@input="updateSlug"
|
@input="updateSlug()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
maxlength="64"
|
maxlength="64"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
@input="setManualSlug"
|
@input="manualSlug = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="hide">
|
<button @click="modal.hide()">
|
||||||
<XIcon aria-hidden="true" />
|
<XIcon aria-hidden="true" />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -70,22 +70,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</NewModal>
|
</NewModal>
|
||||||
</template>
|
</template>
|
||||||
|
<script setup>
|
||||||
<script setup lang="ts">
|
import { XIcon, PlusIcon } from "@modrinth/assets";
|
||||||
import { PlusIcon, XIcon } from "@modrinth/assets";
|
import { ButtonStyled, NewModal } from "@modrinth/ui";
|
||||||
import { ButtonStyled, NewModal, injectNotificationManager } from "@modrinth/ui";
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
const router = useNativeRouter();
|
const router = useNativeRouter();
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
|
|
||||||
const name = ref<string>("");
|
const name = ref("");
|
||||||
const slug = ref<string>("");
|
const slug = ref("");
|
||||||
const description = ref<string>("");
|
const description = ref("");
|
||||||
const manualSlug = ref<boolean>(false);
|
const manualSlug = ref(false);
|
||||||
const modal = ref<InstanceType<typeof NewModal>>();
|
|
||||||
|
|
||||||
async function createOrganization(): Promise<void> {
|
const modal = ref();
|
||||||
|
|
||||||
|
async function createOrganization() {
|
||||||
startLoading();
|
startLoading();
|
||||||
try {
|
try {
|
||||||
const value = {
|
const value = {
|
||||||
@@ -94,18 +92,19 @@ async function createOrganization(): Promise<void> {
|
|||||||
slug: slug.value.trim().replace(/ +/g, ""),
|
slug: slug.value.trim().replace(/ +/g, ""),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result: any = await useBaseFetch("organization", {
|
const result = await useBaseFetch("organization", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(value),
|
body: JSON.stringify(value),
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
modal.value?.hide();
|
modal.value.hide();
|
||||||
|
|
||||||
await router.push(`/organization/${result.slug}`);
|
await router.push(`/organization/${result.slug}`);
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
addNotification({
|
addNotification({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: err.data ? err.data.description : err,
|
text: err.data ? err.data.description : err,
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -113,18 +112,13 @@ async function createOrganization(): Promise<void> {
|
|||||||
}
|
}
|
||||||
stopLoading();
|
stopLoading();
|
||||||
}
|
}
|
||||||
|
function show(event) {
|
||||||
function show(event?: MouseEvent): void {
|
|
||||||
name.value = "";
|
name.value = "";
|
||||||
description.value = "";
|
description.value = "";
|
||||||
modal.value?.show(event);
|
modal.value.show(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide(): void {
|
function updateSlug() {
|
||||||
modal.value?.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSlug(): void {
|
|
||||||
if (!manualSlug.value) {
|
if (!manualSlug.value) {
|
||||||
slug.value = name.value
|
slug.value = name.value
|
||||||
.trim()
|
.trim()
|
||||||
@@ -135,10 +129,6 @@ function updateSlug(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setManualSlug(): void {
|
|
||||||
manualSlug.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
show,
|
show,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,16 +109,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
AsteriskIcon,
|
|
||||||
CheckIcon,
|
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
DropdownIcon,
|
CheckIcon,
|
||||||
LightBulbIcon,
|
|
||||||
ScaleIcon,
|
|
||||||
SendIcon,
|
|
||||||
XIcon,
|
XIcon,
|
||||||
|
AsteriskIcon,
|
||||||
|
LightBulbIcon,
|
||||||
|
SendIcon,
|
||||||
|
ScaleIcon,
|
||||||
|
DropdownIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import { injectNotificationManager } from "@modrinth/ui";
|
|
||||||
import { formatProjectType } from "@modrinth/utils";
|
import { formatProjectType } from "@modrinth/utils";
|
||||||
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
|
||||||
|
|
||||||
@@ -165,8 +164,8 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default() {
|
default() {
|
||||||
return () => {
|
return () => {
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
addNotification({
|
addNotification({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: "setProcessing function not found",
|
text: "setProcessing function not found",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -178,8 +177,8 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default() {
|
default() {
|
||||||
return () => {
|
return () => {
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
addNotification({
|
addNotification({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: "toggleCollapsed function not found",
|
text: "toggleCollapsed function not found",
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -191,8 +190,8 @@ const props = defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
default() {
|
default() {
|
||||||
return () => {
|
return () => {
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
addNotification({
|
addNotification({
|
||||||
|
group: "main",
|
||||||
title: "An error occurred",
|
title: "An error occurred",
|
||||||
text: "updateMembers function not found",
|
text: "updateMembers function not found",
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
+2
-2
@@ -29,7 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import NewModal from "@modrinth/ui/src/components/modal/NewModal.vue";
|
import NewModal from "@modrinth/ui/src/components/modal/NewModal.vue";
|
||||||
import { keybinds, type KeybindListener, normalizeKeybind } from "@modrinth/moderation";
|
import { keybinds, type KeybindListener, normalizeKeybind } from "@modrinth/moderation";
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ function parseKeybindDisplay(keybind: KeybindListener["keybind"]): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isMac() {
|
function isMac() {
|
||||||
return navigator.platform.toUpperCase().includes("MAC");
|
return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function show(event?: MouseEvent) {
|
function show(event?: MouseEvent) {
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="universal-card">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
||||||
<Avatar :src="report.project.icon_url" size="3rem" class="flex-shrink-0" />
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<h3 class="truncate text-lg font-semibold">{{ report.project.title }}</h3>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 text-sm text-secondary sm:flex-row sm:items-center">
|
|
||||||
<nuxt-link
|
|
||||||
v-if="report.target"
|
|
||||||
:to="`/${report.target.type}/${report.target.slug}`"
|
|
||||||
class="inline-flex flex-row items-center gap-1 transition-colors duration-100 ease-in-out hover:text-brand"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
:src="report.target.avatar_url"
|
|
||||||
:circle="report.target.type === 'user'"
|
|
||||||
size="1rem"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span class="truncate">
|
|
||||||
<OrganizationIcon
|
|
||||||
v-if="report.target.type === 'organization'"
|
|
||||||
class="align-middle"
|
|
||||||
/>
|
|
||||||
{{ report.target.name }}
|
|
||||||
</span>
|
|
||||||
</nuxt-link>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap rounded-full bg-button-bg p-0.5 px-2 text-xs font-semibold text-secondary"
|
|
||||||
>
|
|
||||||
Score: {{ report.priority_score }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap rounded-full bg-button-bg p-0.5 px-2 text-xs font-semibold"
|
|
||||||
:class="{
|
|
||||||
'text-brand': report.status === 'approved',
|
|
||||||
'text-red': report.status === 'rejected',
|
|
||||||
'text-secondary': report.status === 'pending',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ report.status.charAt(0).toUpperCase() + report.status.slice(1) }}
|
|
||||||
</span>
|
|
||||||
<span class="max-w-[200px] truncate font-mono text-xs sm:max-w-none">
|
|
||||||
{{
|
|
||||||
report.version.files.find((file) => file.primary)?.filename ||
|
|
||||||
"Unknown primary file"
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="mt-2 flex flex-col items-stretch gap-2 sm:mt-0 sm:flex-row sm:items-center sm:gap-2"
|
|
||||||
>
|
|
||||||
<span class="hidden whitespace-nowrap text-sm text-secondary sm:block">
|
|
||||||
{{ formatRelativeTime(dayjs(report.detected_at).toDate()) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 sm:flex-row">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<ButtonStyled class="flex-1 sm:flex-none">
|
|
||||||
<button
|
|
||||||
v-tooltip="!isPending ? 'This report has already been dealt with.' : undefined"
|
|
||||||
:disabled="!isPending"
|
|
||||||
class="w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
Accept
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled class="flex-1 sm:flex-none">
|
|
||||||
<button
|
|
||||||
v-tooltip="!isPending ? 'This report has already been dealt with.' : undefined"
|
|
||||||
:disabled="!isPending"
|
|
||||||
class="w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
Reject
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-center gap-2 sm:justify-start">
|
|
||||||
<ButtonStyled circular>
|
|
||||||
<nuxt-link :to="versionUrl">
|
|
||||||
<EyeIcon />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled circular>
|
|
||||||
<OverflowMenu :options="quickActions">
|
|
||||||
<template #default>
|
|
||||||
<EllipsisVerticalIcon />
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
|
||||||
<ClipboardCopyIcon />
|
|
||||||
<span class="hidden sm:inline">Copy ID</span>
|
|
||||||
</template>
|
|
||||||
<template #copy-link>
|
|
||||||
<LinkIcon />
|
|
||||||
<span class="hidden sm:inline">Copy link</span>
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm text-secondary sm:hidden">
|
|
||||||
{{ formatRelativeTime(dayjs(report.detected_at).toDate()) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ClipboardCopyIcon,
|
|
||||||
EllipsisVerticalIcon,
|
|
||||||
EyeIcon,
|
|
||||||
LinkIcon,
|
|
||||||
OrganizationIcon,
|
|
||||||
} from "@modrinth/assets";
|
|
||||||
import type { ExtendedDelphiReport } from "@modrinth/moderation";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
ButtonStyled,
|
|
||||||
injectNotificationManager,
|
|
||||||
OverflowMenu,
|
|
||||||
useRelativeTime,
|
|
||||||
type OverflowMenuOption,
|
|
||||||
} from "@modrinth/ui";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
report: ExtendedDelphiReport;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
|
||||||
const isPending = computed(() => props.report.status === "pending");
|
|
||||||
|
|
||||||
const quickActions: OverflowMenuOption[] = [
|
|
||||||
{
|
|
||||||
id: "copy-link",
|
|
||||||
action: () => {
|
|
||||||
const base = window.location.origin;
|
|
||||||
const reviewUrl = `${base}/moderation/tech-reviews?q=${props.report.version.id}`;
|
|
||||||
navigator.clipboard.writeText(reviewUrl).then(() => {
|
|
||||||
addNotification({
|
|
||||||
type: "success",
|
|
||||||
title: "Tech review link copied",
|
|
||||||
text: "The link to this tech review has been copied to your clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "copy-id",
|
|
||||||
action: () => {
|
|
||||||
navigator.clipboard.writeText(props.report.version.id).then(() => {
|
|
||||||
addNotification({
|
|
||||||
type: "success",
|
|
||||||
title: "Version ID copied",
|
|
||||||
text: "The ID of this version has been copied to your clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const versionUrl = computed(() => {
|
|
||||||
return `/${props.report.project.project_type}/${props.report.project.slug}/versions/${props.report.version.id}`;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="universal-card flex min-h-[6rem] flex-col justify-between gap-3 rounded-lg p-4 sm:h-24 sm:flex-row sm:items-center sm:gap-0"
|
|
||||||
>
|
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
||||||
<div class="flex-shrink-0 rounded-lg">
|
|
||||||
<Avatar size="48px" :src="queueEntry.project.icon_url" />
|
|
||||||
</div>
|
|
||||||
<div class="flex min-w-0 flex-1 flex-col">
|
|
||||||
<h3 class="truncate text-lg font-semibold">
|
|
||||||
{{ queueEntry.project.name }}
|
|
||||||
</h3>
|
|
||||||
<nuxt-link
|
|
||||||
v-if="queueEntry.owner"
|
|
||||||
target="_blank"
|
|
||||||
class="flex items-center gap-1 truncate align-middle text-sm hover:text-brand"
|
|
||||||
:to="`/user/${queueEntry.owner.user.username}`"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
:src="queueEntry.owner.user.avatar_url"
|
|
||||||
circle
|
|
||||||
size="16px"
|
|
||||||
class="inline-block flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span class="truncate">{{ queueEntry.owner.user.username }}</span>
|
|
||||||
</nuxt-link>
|
|
||||||
<nuxt-link
|
|
||||||
v-else-if="queueEntry.org"
|
|
||||||
target="_blank"
|
|
||||||
class="flex items-center gap-1 truncate align-middle text-sm hover:text-brand"
|
|
||||||
:to="`/organization/${queueEntry.org.slug}`"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
:src="queueEntry.org.icon_url"
|
|
||||||
circle
|
|
||||||
size="16px"
|
|
||||||
class="inline-block flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span class="truncate">{{ queueEntry.org.name }}</span>
|
|
||||||
</nuxt-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-4">
|
|
||||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-1">
|
|
||||||
<span class="flex items-center gap-1 whitespace-nowrap text-sm">
|
|
||||||
<BoxIcon
|
|
||||||
v-if="queueEntry.project.project_type === 'mod'"
|
|
||||||
class="size-4 flex-shrink-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<PaintbrushIcon
|
|
||||||
v-else-if="queueEntry.project.project_type === 'resourcepack'"
|
|
||||||
class="size-4 flex-shrink-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<BracesIcon
|
|
||||||
v-else-if="queueEntry.project.project_type === 'datapack'"
|
|
||||||
class="size-4 flex-shrink-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<PackageOpenIcon
|
|
||||||
v-else-if="queueEntry.project.project_type === 'modpack'"
|
|
||||||
class="size-4 flex-shrink-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<GlassesIcon
|
|
||||||
v-else-if="queueEntry.project.project_type === 'shader'"
|
|
||||||
class="size-4 flex-shrink-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<PlugIcon
|
|
||||||
v-else-if="queueEntry.project.project_type === 'plugin'"
|
|
||||||
class="size-4 flex-shrink-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span class="hidden sm:inline">{{
|
|
||||||
props.queueEntry.project.project_types.map(formatProjectType).join(", ")
|
|
||||||
}}</span>
|
|
||||||
<span class="sm:hidden">{{
|
|
||||||
formatProjectType(props.queueEntry.project.project_type ?? "project").substring(0, 3)
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="hidden text-sm sm:inline">•</span>
|
|
||||||
|
|
||||||
<div class="flex flex-row gap-2 text-sm">
|
|
||||||
Requesting
|
|
||||||
<Badge
|
|
||||||
v-if="props.queueEntry.project.requested_status"
|
|
||||||
:type="props.queueEntry.project.requested_status"
|
|
||||||
class="status"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="hidden text-sm sm:inline">•</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
v-tooltip="`Since ${queuedDate.toLocaleString()}`"
|
|
||||||
class="truncate text-sm"
|
|
||||||
:class="{
|
|
||||||
'text-red': daysInQueue > 4,
|
|
||||||
'text-orange': daysInQueue > 2,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="hidden sm:inline">{{ getSubmittedTime(queueEntry) }}</span>
|
|
||||||
<span class="sm:hidden">{{
|
|
||||||
getSubmittedTime(queueEntry).replace("Submitted ", "")
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-2 sm:justify-start">
|
|
||||||
<ButtonStyled circular>
|
|
||||||
<NuxtLink target="_blank" :to="`/project/${queueEntry.project.slug}`">
|
|
||||||
<EyeIcon class="size-4" />
|
|
||||||
</NuxtLink>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled circular color="orange" @click="openProjectForReview">
|
|
||||||
<button>
|
|
||||||
<ScaleIcon class="size-4" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import {
|
|
||||||
EyeIcon,
|
|
||||||
PaintbrushIcon,
|
|
||||||
ScaleIcon,
|
|
||||||
BoxIcon,
|
|
||||||
GlassesIcon,
|
|
||||||
PlugIcon,
|
|
||||||
PackageOpenIcon,
|
|
||||||
BracesIcon,
|
|
||||||
} from "@modrinth/assets";
|
|
||||||
import { useRelativeTime, Avatar, ButtonStyled, Badge } from "@modrinth/ui";
|
|
||||||
import {
|
|
||||||
formatProjectType,
|
|
||||||
type Organization,
|
|
||||||
type Project,
|
|
||||||
type TeamMember,
|
|
||||||
} from "@modrinth/utils";
|
|
||||||
import { computed } from "vue";
|
|
||||||
import { useModerationStore } from "~/store/moderation.ts";
|
|
||||||
import type { ModerationProject } from "~/helpers/moderation";
|
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
|
||||||
const moderationStore = useModerationStore();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
queueEntry: ModerationProject;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function getDaysQueued(date: Date): number {
|
|
||||||
const now = new Date();
|
|
||||||
const diff = now.getTime() - date.getTime();
|
|
||||||
return Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
||||||
}
|
|
||||||
|
|
||||||
const queuedDate = computed(() => {
|
|
||||||
return dayjs(
|
|
||||||
props.queueEntry.project.queued ||
|
|
||||||
props.queueEntry.project.created ||
|
|
||||||
props.queueEntry.project.updated,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const daysInQueue = computed(() => {
|
|
||||||
return getDaysQueued(queuedDate.value.toDate());
|
|
||||||
});
|
|
||||||
|
|
||||||
function openProjectForReview() {
|
|
||||||
moderationStore.setSingleProject(props.queueEntry.project.id);
|
|
||||||
navigateTo({
|
|
||||||
name: "type-id",
|
|
||||||
params: {
|
|
||||||
type: "project",
|
|
||||||
id: props.queueEntry.project.id,
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
showChecklist: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubmittedTime(project: any): string {
|
|
||||||
const date =
|
|
||||||
props.queueEntry.project.queued ||
|
|
||||||
props.queueEntry.project.created ||
|
|
||||||
props.queueEntry.project.updated;
|
|
||||||
if (!date) return "Unknown";
|
|
||||||
|
|
||||||
try {
|
|
||||||
return `Submitted ${formatRelativeTime(dayjs(date).toISOString())}`;
|
|
||||||
} catch {
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="universal-card">
|
|
||||||
<div
|
|
||||||
class="flex w-full flex-col items-start justify-between gap-3 sm:flex-row sm:items-center sm:gap-0"
|
|
||||||
>
|
|
||||||
<span class="text-md flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
Reported for
|
|
||||||
<span class="whitespace-nowrap rounded-full align-middle font-semibold text-contrast">
|
|
||||||
{{ formattedReportType }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<span class="hidden sm:inline">By</span>
|
|
||||||
<span class="sm:hidden">Reporter:</span>
|
|
||||||
<nuxt-link
|
|
||||||
:to="`/user/${report.reporter_user.username}`"
|
|
||||||
class="inline-flex flex-row items-center gap-1 transition-colors duration-100 ease-in-out hover:text-brand"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
:src="report.reporter_user.avatar_url"
|
|
||||||
circle
|
|
||||||
size="1.75rem"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span class="truncate">{{ report.reporter_user.username }}</span>
|
|
||||||
</nuxt-link>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="flex flex-row items-center gap-2 self-end sm:self-auto">
|
|
||||||
<span class="text-md whitespace-nowrap text-secondary">{{
|
|
||||||
formatRelativeTime(report.created)
|
|
||||||
}}</span>
|
|
||||||
<ButtonStyled v-if="visibleQuickReplies.length > 0" circular>
|
|
||||||
<OverflowMenu :options="visibleQuickReplies">
|
|
||||||
<span class="hidden sm:inline">Quick Reply</span>
|
|
||||||
<span class="sr-only sm:hidden">Quick Reply</span>
|
|
||||||
<ChevronDownIcon />
|
|
||||||
</OverflowMenu>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled circular>
|
|
||||||
<OverflowMenu :options="quickActions">
|
|
||||||
<template #default>
|
|
||||||
<EllipsisVerticalIcon />
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
|
||||||
<ClipboardCopyIcon />
|
|
||||||
<span class="hidden sm:inline">Copy ID</span>
|
|
||||||
</template>
|
|
||||||
<template #copy-link>
|
|
||||||
<LinkIcon />
|
|
||||||
<span class="hidden sm:inline">Copy link</span>
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-4 rounded-xl border-solid text-divider" />
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center">
|
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
||||||
<Avatar
|
|
||||||
:src="reportItemAvatarUrl"
|
|
||||||
:circle="report.item_type === 'user'"
|
|
||||||
size="3rem"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<span class="block truncate text-lg font-semibold">{{ reportItemTitle }}</span>
|
|
||||||
<div class="flex flex-col gap-2 text-sm text-secondary sm:flex-row sm:items-center">
|
|
||||||
<nuxt-link
|
|
||||||
v-if="report.target && report.item_type != 'user'"
|
|
||||||
:to="`/${report.target.type}/${report.target.slug}`"
|
|
||||||
class="inline-flex flex-row items-center gap-1 truncate transition-colors duration-100 ease-in-out hover:text-brand"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
:src="report.target?.avatar_url"
|
|
||||||
:circle="report.target.type === 'user'"
|
|
||||||
size="1rem"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span class="truncate">
|
|
||||||
<OrganizationIcon
|
|
||||||
v-if="report.target.type === 'organization'"
|
|
||||||
class="align-middle"
|
|
||||||
/>
|
|
||||||
{{ report.target.name || "Unknown User" }}
|
|
||||||
</span>
|
|
||||||
</nuxt-link>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<span
|
|
||||||
class="whitespace-nowrap rounded-full bg-button-bg p-0.5 px-2 text-xs font-semibold text-secondary"
|
|
||||||
>
|
|
||||||
{{ formattedItemType }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="report.item_type === 'version' && report.version"
|
|
||||||
class="max-w-[200px] truncate font-mono text-xs sm:max-w-none"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
report.version.files.find((file) => file.primary)?.filename || "Unknown Version"
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end sm:justify-start">
|
|
||||||
<ButtonStyled circular>
|
|
||||||
<nuxt-link :to="reportItemUrl">
|
|
||||||
<EyeIcon />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CollapsibleRegion class="my-4" ref="collapsibleRegion">
|
|
||||||
<ReportThread
|
|
||||||
v-if="report.thread"
|
|
||||||
ref="reportThread"
|
|
||||||
class="mb-16 sm:mb-0"
|
|
||||||
:thread="report.thread"
|
|
||||||
:report="report"
|
|
||||||
:reporter="report.reporter_user"
|
|
||||||
@update-thread="updateThread"
|
|
||||||
/>
|
|
||||||
</CollapsibleRegion>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ClipboardCopyIcon,
|
|
||||||
EllipsisVerticalIcon,
|
|
||||||
EyeIcon,
|
|
||||||
LinkIcon,
|
|
||||||
OrganizationIcon,
|
|
||||||
} from "@modrinth/assets";
|
|
||||||
import {
|
|
||||||
type ExtendedReport,
|
|
||||||
reportQuickReplies,
|
|
||||||
type ReportQuickReply,
|
|
||||||
} from "@modrinth/moderation";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
ButtonStyled,
|
|
||||||
CollapsibleRegion,
|
|
||||||
injectNotificationManager,
|
|
||||||
OverflowMenu,
|
|
||||||
type OverflowMenuOption,
|
|
||||||
useRelativeTime,
|
|
||||||
} from "@modrinth/ui";
|
|
||||||
import ChevronDownIcon from "../servers/icons/ChevronDownIcon.vue";
|
|
||||||
import ReportThread from "../thread/ReportThread.vue";
|
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager();
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
report: ExtendedReport;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const reportThread = ref<InstanceType<typeof ReportThread> | null>(null);
|
|
||||||
const collapsibleRegion = ref<InstanceType<typeof CollapsibleRegion> | null>(null);
|
|
||||||
|
|
||||||
const formatRelativeTime = useRelativeTime();
|
|
||||||
|
|
||||||
function updateThread(newThread: any) {
|
|
||||||
if (props.report.thread) {
|
|
||||||
Object.assign(props.report.thread, newThread);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const quickActions: OverflowMenuOption[] = [
|
|
||||||
{
|
|
||||||
id: "copy-link",
|
|
||||||
action: () => {
|
|
||||||
const base = window.location.origin;
|
|
||||||
const reportUrl = `${base}/moderation/reports/${props.report.id}`;
|
|
||||||
navigator.clipboard.writeText(reportUrl).then(() => {
|
|
||||||
addNotification({
|
|
||||||
type: "success",
|
|
||||||
title: "Report link copied",
|
|
||||||
text: "The link to this report has been copied to your clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "copy-id",
|
|
||||||
action: () => {
|
|
||||||
navigator.clipboard.writeText(props.report.id).then(() => {
|
|
||||||
addNotification({
|
|
||||||
type: "success",
|
|
||||||
title: "Report ID copied",
|
|
||||||
text: "The ID of this report has been copied to your clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const visibleQuickReplies = computed<OverflowMenuOption[]>(() => {
|
|
||||||
return reportQuickReplies
|
|
||||||
.filter((reply) => {
|
|
||||||
if (reply.shouldShow === undefined) return true;
|
|
||||||
if (typeof reply.shouldShow === "function") {
|
|
||||||
return reply.shouldShow(props.report);
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply.shouldShow;
|
|
||||||
})
|
|
||||||
.map(
|
|
||||||
(reply) =>
|
|
||||||
({
|
|
||||||
id: reply.label,
|
|
||||||
action: () => handleQuickReply(reply),
|
|
||||||
}) as OverflowMenuOption,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleQuickReply(reply: ReportQuickReply) {
|
|
||||||
const message =
|
|
||||||
typeof reply.message === "function" ? await reply.message(props.report) : reply.message;
|
|
||||||
|
|
||||||
collapsibleRegion.value?.setCollapsed(false);
|
|
||||||
await nextTick();
|
|
||||||
reportThread.value?.setReplyContent(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportItemAvatarUrl = computed(() => {
|
|
||||||
switch (props.report.item_type) {
|
|
||||||
case "project":
|
|
||||||
case "version":
|
|
||||||
return props.report.project?.icon_url || "";
|
|
||||||
case "user":
|
|
||||||
return props.report.user?.avatar_url || "";
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const reportItemTitle = computed(() => {
|
|
||||||
if (props.report.item_type === "user") return props.report.user?.username || "Unknown User";
|
|
||||||
|
|
||||||
return props.report.project?.title || "Unknown Project";
|
|
||||||
});
|
|
||||||
|
|
||||||
const reportItemUrl = computed(() => {
|
|
||||||
switch (props.report.item_type) {
|
|
||||||
case "user":
|
|
||||||
return `/user/${props.report.user?.username}`;
|
|
||||||
case "project":
|
|
||||||
return `/${props.report.project?.project_type}/${props.report.project?.slug}`;
|
|
||||||
case "version":
|
|
||||||
return `/${props.report.project?.project_type}/${props.report.project?.slug}/versions/${props.report.version?.id}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedItemType = computed(() => {
|
|
||||||
const itemType = props.report.item_type;
|
|
||||||
return itemType.charAt(0).toUpperCase() + itemType.slice(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const formattedReportType = computed(() => {
|
|
||||||
const reportType = props.report.report_type;
|
|
||||||
|
|
||||||
// some are split by -, some are split by " "
|
|
||||||
const words = reportType.includes("-") ? reportType.split("-") : reportType.split(" ");
|
|
||||||
return words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
+3
-94
@@ -8,7 +8,7 @@
|
|||||||
<div v-if="!modPackData">Loading data...</div>
|
<div v-if="!modPackData">Loading data...</div>
|
||||||
|
|
||||||
<div v-else-if="modPackData.length === 0">
|
<div v-else-if="modPackData.length === 0">
|
||||||
<p>All permissions already obtained.</p>
|
<p>All permissions obtained. You may skip this step!</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!modPackData[currentIndex]">
|
<div v-else-if="!modPackData[currentIndex]">
|
||||||
@@ -157,7 +157,7 @@ import type {
|
|||||||
} from "@modrinth/utils";
|
} from "@modrinth/utils";
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { ButtonStyled } from "@modrinth/ui";
|
||||||
import { ref, computed, watch, onMounted } from "vue";
|
import { ref, computed, watch, onMounted } from "vue";
|
||||||
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -182,26 +182,7 @@ const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
|||||||
|
|
||||||
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0);
|
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0);
|
||||||
|
|
||||||
const modPackData = useSessionStorage<ModerationModpackItem[] | null>(
|
const modPackData = ref<ModerationModpackItem[] | null>(null);
|
||||||
`modpack-permissions-data-${props.projectId}`,
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
serializer: {
|
|
||||||
read: (v: any) => (v ? JSON.parse(v) : null),
|
|
||||||
write: (v: any) => JSON.stringify(v),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const permanentNoFiles = useSessionStorage<ModerationModpackItem[]>(
|
|
||||||
`modpack-permissions-permanent-no-${props.projectId}`,
|
|
||||||
[],
|
|
||||||
{
|
|
||||||
serializer: {
|
|
||||||
read: (v: any) => (v ? JSON.parse(v) : []),
|
|
||||||
write: (v: any) => JSON.stringify(v),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const currentIndex = ref(0);
|
const currentIndex = ref(0);
|
||||||
|
|
||||||
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
||||||
@@ -270,45 +251,7 @@ async function fetchModPackData(): Promise<void> {
|
|||||||
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
||||||
internal: true,
|
internal: true,
|
||||||
})) as ModerationModpackResponse;
|
})) as ModerationModpackResponse;
|
||||||
|
|
||||||
const permanentNoItems: ModerationModpackItem[] = Object.entries(data.identified || {})
|
|
||||||
.filter(([_, file]) => file.status === "permanent-no")
|
|
||||||
.map(
|
|
||||||
([sha1, file]): ModerationModpackItem => ({
|
|
||||||
sha1,
|
|
||||||
file_name: file.file_name,
|
|
||||||
type: "identified",
|
|
||||||
status: file.status,
|
|
||||||
approved: null,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.file_name.localeCompare(b.file_name));
|
|
||||||
|
|
||||||
permanentNoFiles.value = permanentNoItems;
|
|
||||||
|
|
||||||
const sortedData: ModerationModpackItem[] = [
|
const sortedData: ModerationModpackItem[] = [
|
||||||
...Object.entries(data.identified || {})
|
|
||||||
.filter(
|
|
||||||
([_, file]) =>
|
|
||||||
file.status !== "yes" &&
|
|
||||||
file.status !== "with-attribution-and-source" &&
|
|
||||||
file.status !== "permanent-no",
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
([sha1, file]): ModerationModpackItem => ({
|
|
||||||
sha1,
|
|
||||||
file_name: file.file_name,
|
|
||||||
type: "identified",
|
|
||||||
status: file.status,
|
|
||||||
approved: null,
|
|
||||||
...(file.status === "unidentified" && {
|
|
||||||
proof: "",
|
|
||||||
url: "",
|
|
||||||
title: "",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
|
||||||
...Object.entries(data.unknown_files || {})
|
...Object.entries(data.unknown_files || {})
|
||||||
.map(
|
.map(
|
||||||
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
||||||
@@ -367,7 +310,6 @@ async function fetchModPackData(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch modpack data:", error);
|
console.error("Failed to fetch modpack data:", error);
|
||||||
modPackData.value = [];
|
modPackData.value = [];
|
||||||
permanentNoFiles.value = [];
|
|
||||||
persistAll();
|
persistAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,14 +321,6 @@ function goToPrevious(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
modPackData,
|
|
||||||
(newValue) => {
|
|
||||||
persistedModPackData.value = newValue;
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
function goToNext(): void {
|
function goToNext(): void {
|
||||||
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
||||||
currentIndex.value++;
|
currentIndex.value++;
|
||||||
@@ -462,17 +396,6 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
|
||||||
modPackData,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue && newValue.length === 0) {
|
|
||||||
emit("complete");
|
|
||||||
clearPersistedData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.projectId,
|
() => props.projectId,
|
||||||
() => {
|
() => {
|
||||||
@@ -483,20 +406,6 @@ watch(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function getModpackFiles(): {
|
|
||||||
interactive: ModerationModpackItem[];
|
|
||||||
permanentNo: ModerationModpackItem[];
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
interactive: modPackData.value || [],
|
|
||||||
permanentNo: permanentNoFiles.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
getModpackFiles,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
+162
-207
@@ -42,9 +42,9 @@
|
|||||||
<div v-if="done">
|
<div v-if="done">
|
||||||
<p>
|
<p>
|
||||||
You are done moderating this project!
|
You are done moderating this project!
|
||||||
<template v-if="moderationStore.hasItems">
|
<template v-if="futureProjectCount > 0">
|
||||||
There are
|
There are
|
||||||
{{ moderationStore.queueLength }} left.
|
{{ futureProjectCount }} left.
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<div v-if="toggleActions.length > 0" class="toggle-actions-group space-y-3">
|
<div v-if="toggleActions.length > 0" class="toggle-actions-group space-y-3">
|
||||||
<template v-for="action in toggleActions" :key="getActionKey(action)">
|
<template v-for="action in toggleActions" :key="getActionKey(action)">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="isActionSelected(action)"
|
:model-value="actionStates[getActionId(action)]?.selected ?? false"
|
||||||
:label="action.label"
|
:label="action.label"
|
||||||
:description="action.description"
|
:description="action.description"
|
||||||
:disabled="false"
|
:disabled="false"
|
||||||
@@ -215,31 +215,49 @@
|
|||||||
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
|
class="mt-4 flex grow justify-between gap-2 border-0 border-t-[1px] border-solid border-divider pt-4"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<ButtonStyled v-if="!done && !generatedMessage && moderationStore.hasItems">
|
<ButtonStyled v-if="!done && !generatedMessage && futureProjectCount > 0">
|
||||||
<button @click="skipCurrentProject">
|
<button @click="goToNextProject">
|
||||||
<XIcon aria-hidden="true" />
|
<XIcon aria-hidden="true" />
|
||||||
Skip ({{ moderationStore.queueLength }} left)
|
Skip
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="done">
|
<div v-if="done">
|
||||||
<ButtonStyled color="brand">
|
<ButtonStyled v-if="futureProjectCount > 0" color="brand">
|
||||||
<button @click="endChecklist(undefined)">
|
<button @click="goToNextProject">
|
||||||
<template v-if="hasNextProject">
|
<RightArrowIcon aria-hidden="true" />
|
||||||
<RightArrowIcon aria-hidden="true" />
|
Next Project
|
||||||
Next Project ({{ moderationStore.queueLength }} left)
|
</button>
|
||||||
</template>
|
</ButtonStyled>
|
||||||
<template v-else>
|
<ButtonStyled v-else color="brand">
|
||||||
<CheckIcon aria-hidden="true" />
|
<button @click="exitModeration">
|
||||||
All Done!
|
<CheckIcon aria-hidden="true" />
|
||||||
</template>
|
Done
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="generatedMessage" class="flex items-center gap-2">
|
<div v-else-if="generatedMessage" class="flex items-center gap-2">
|
||||||
|
<OverflowMenu :options="stageOptions" class="bg-transparent p-0">
|
||||||
|
<ButtonStyled circular>
|
||||||
|
<button v-tooltip="`Stages`">
|
||||||
|
<ListBulletedIcon />
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-for="opt in stageOptions.filter(
|
||||||
|
(opt) => 'id' in opt && 'text' in opt && 'icon' in opt,
|
||||||
|
)"
|
||||||
|
#[opt.id]
|
||||||
|
:key="opt.id"
|
||||||
|
>
|
||||||
|
<component :is="opt.icon" v-if="opt.icon" class="mr-2" />
|
||||||
|
{{ opt.text }}
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button @click="goBackToStages">
|
<button @click="goBackToStages">
|
||||||
<LeftArrowIcon aria-hidden="true" />
|
<LeftArrowIcon aria-hidden="true" />
|
||||||
@@ -259,7 +277,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled color="green">
|
<ButtonStyled color="green">
|
||||||
<button @click="sendMessage(project.requested_status ?? 'approved')">
|
<button @click="sendMessage('approved')">
|
||||||
<CheckIcon aria-hidden="true" />
|
<CheckIcon aria-hidden="true" />
|
||||||
Approve
|
Approve
|
||||||
</button>
|
</button>
|
||||||
@@ -313,80 +331,81 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
|
LeftArrowIcon,
|
||||||
|
RightArrowIcon,
|
||||||
|
DropdownIcon,
|
||||||
|
XIcon,
|
||||||
|
ScaleIcon,
|
||||||
|
ListBulletedIcon,
|
||||||
|
FileTextIcon,
|
||||||
BrushCleaningIcon,
|
BrushCleaningIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
DropdownIcon,
|
|
||||||
EyeOffIcon,
|
|
||||||
FileTextIcon,
|
|
||||||
KeyboardIcon,
|
KeyboardIcon,
|
||||||
LeftArrowIcon,
|
EyeOffIcon,
|
||||||
ListBulletedIcon,
|
|
||||||
RightArrowIcon,
|
|
||||||
ScaleIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
import {
|
import {
|
||||||
type Action,
|
|
||||||
type ButtonAction,
|
|
||||||
type ConditionalButtonAction,
|
|
||||||
type DropdownAction,
|
|
||||||
type MultiSelectChipsAction,
|
|
||||||
type Stage,
|
|
||||||
type ToggleAction,
|
|
||||||
checklist,
|
checklist,
|
||||||
deserializeActionStates,
|
|
||||||
expandVariables,
|
|
||||||
finalPermissionMessages,
|
|
||||||
findMatchingVariant,
|
|
||||||
flattenProjectVariables,
|
|
||||||
getActionIdForStage,
|
getActionIdForStage,
|
||||||
getActionMessage,
|
|
||||||
getVisibleInputs,
|
|
||||||
handleKeybind,
|
|
||||||
initializeActionState,
|
initializeActionState,
|
||||||
kebabToTitleCase,
|
getActionMessage,
|
||||||
keybinds,
|
findMatchingVariant,
|
||||||
processMessage,
|
processMessage,
|
||||||
|
getVisibleInputs,
|
||||||
serializeActionStates,
|
serializeActionStates,
|
||||||
|
deserializeActionStates,
|
||||||
|
kebabToTitleCase,
|
||||||
|
flattenProjectVariables,
|
||||||
|
expandVariables,
|
||||||
|
handleKeybind,
|
||||||
|
keybinds,
|
||||||
} from "@modrinth/moderation";
|
} from "@modrinth/moderation";
|
||||||
import {
|
import {
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
Checkbox,
|
|
||||||
Collapsible,
|
Collapsible,
|
||||||
DropdownSelect,
|
|
||||||
MarkdownEditor,
|
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
type OverflowMenuOption,
|
type OverflowMenuOption,
|
||||||
injectNotificationManager,
|
Checkbox,
|
||||||
|
DropdownSelect,
|
||||||
|
MarkdownEditor,
|
||||||
} from "@modrinth/ui";
|
} from "@modrinth/ui";
|
||||||
import {
|
import { type Project, renderHighlightedString, type ModerationJudgements } from "@modrinth/utils";
|
||||||
type ModerationJudgements,
|
|
||||||
type ModerationModpackItem,
|
|
||||||
type Project,
|
|
||||||
renderHighlightedString,
|
|
||||||
type ProjectStatus,
|
|
||||||
} from "@modrinth/utils";
|
|
||||||
import { computedAsync, useLocalStorage } from "@vueuse/core";
|
import { computedAsync, useLocalStorage } from "@vueuse/core";
|
||||||
import * as prettier from "prettier";
|
import type {
|
||||||
import { useModerationStore } from "~/store/moderation.ts";
|
Action,
|
||||||
import KeybindsModal from "./ChecklistKeybindsModal.vue";
|
MultiSelectChipsAction,
|
||||||
|
DropdownAction,
|
||||||
|
ButtonAction,
|
||||||
|
ToggleAction,
|
||||||
|
ConditionalButtonAction,
|
||||||
|
Stage,
|
||||||
|
} from "@modrinth/moderation";
|
||||||
import ModpackPermissionsFlow from "./ModpackPermissionsFlow.vue";
|
import ModpackPermissionsFlow from "./ModpackPermissionsFlow.vue";
|
||||||
|
import KeybindsModal from "./ChecklistKeybindsModal.vue";
|
||||||
const { addNotification } = injectNotificationManager();
|
import { finalPermissionMessages } from "@modrinth/moderation/data/modpack-permissions-stage";
|
||||||
|
import prettier from "prettier";
|
||||||
|
|
||||||
const keybindsModal = ref<InstanceType<typeof KeybindsModal>>();
|
const keybindsModal = ref<InstanceType<typeof KeybindsModal>>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
project: Project;
|
defineProps<{
|
||||||
collapsed: boolean;
|
project: Project;
|
||||||
}>();
|
futureProjectIds?: string[];
|
||||||
|
collapsed: boolean;
|
||||||
const moderationStore = useModerationStore();
|
}>(),
|
||||||
|
{
|
||||||
|
futureProjectIds: () => [] as string[],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const variables = computed(() => {
|
const variables = computed(() => {
|
||||||
return flattenProjectVariables(props.project);
|
return flattenProjectVariables(props.project);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const futureProjectCount = computed(() => {
|
||||||
|
const ids = JSON.parse(localStorage.getItem("moderation-future-projects") || "[]");
|
||||||
|
return ids.length;
|
||||||
|
});
|
||||||
|
|
||||||
const modpackPermissionsComplete = ref(false);
|
const modpackPermissionsComplete = ref(false);
|
||||||
const modpackJudgements = ref<ModerationJudgements>({});
|
const modpackJudgements = ref<ModerationJudgements>({});
|
||||||
const isModpackPermissionsStage = computed(() => {
|
const isModpackPermissionsStage = computed(() => {
|
||||||
@@ -400,6 +419,7 @@ const done = ref(false);
|
|||||||
|
|
||||||
function handleModpackPermissionsComplete() {
|
function handleModpackPermissionsComplete() {
|
||||||
modpackPermissionsComplete.value = true;
|
modpackPermissionsComplete.value = true;
|
||||||
|
nextStage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -510,7 +530,7 @@ function handleKeybinds(event: KeyboardEvent) {
|
|||||||
isLoadingMessage: loadingMessage.value,
|
isLoadingMessage: loadingMessage.value,
|
||||||
isModpackPermissionsStage: isModpackPermissionsStage.value,
|
isModpackPermissionsStage: isModpackPermissionsStage.value,
|
||||||
|
|
||||||
futureProjectCount: moderationStore.queueLength,
|
futureProjectCount: futureProjectCount.value,
|
||||||
visibleActionsCount: visibleActions.value.length,
|
visibleActionsCount: visibleActions.value.length,
|
||||||
|
|
||||||
focusedActionIndex: focusedActionIndex.value,
|
focusedActionIndex: focusedActionIndex.value,
|
||||||
@@ -523,13 +543,13 @@ function handleKeybinds(event: KeyboardEvent) {
|
|||||||
tryGoNext: nextStage,
|
tryGoNext: nextStage,
|
||||||
tryGoBack: previousStage,
|
tryGoBack: previousStage,
|
||||||
tryGenerateMessage: generateMessage,
|
tryGenerateMessage: generateMessage,
|
||||||
trySkipProject: skipCurrentProject,
|
trySkipProject: goToNextProject,
|
||||||
|
|
||||||
tryToggleCollapse: () => emit("toggleCollapsed"),
|
tryToggleCollapse: () => emit("toggleCollapsed"),
|
||||||
tryResetProgress: resetProgress,
|
tryResetProgress: resetProgress,
|
||||||
tryExitModeration: () => emit("exit"),
|
tryExitModeration: () => emit("exit"),
|
||||||
|
|
||||||
tryApprove: () => sendMessage(props.project.requested_status),
|
tryApprove: () => sendMessage("approved"),
|
||||||
tryReject: () => sendMessage("rejected"),
|
tryReject: () => sendMessage("rejected"),
|
||||||
tryWithhold: () => sendMessage("withheld"),
|
tryWithhold: () => sendMessage("withheld"),
|
||||||
tryEditMessage: goBackToStages,
|
tryEditMessage: goBackToStages,
|
||||||
@@ -646,17 +666,12 @@ function initializeStageActions(stage: Stage, stageIndex: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActionId(action: Action, index?: number): string {
|
function getActionId(action: Action, index?: number): string {
|
||||||
// If index is not provided, find it in the current stage's actions
|
|
||||||
if (index === undefined) {
|
|
||||||
index = currentStageObj.value.actions.indexOf(action);
|
|
||||||
}
|
|
||||||
return getActionIdForStage(action, currentStage.value, index);
|
return getActionIdForStage(action, currentStage.value, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActionKey(action: Action): string {
|
function getActionKey(action: Action): string {
|
||||||
// Find the actual index of this action in the current stage's actions array
|
const index = visibleActions.value.indexOf(action);
|
||||||
const index = currentStageObj.value.actions.indexOf(action);
|
return `${currentStage.value}-${index}-${getActionId(action)}`;
|
||||||
return `${currentStage.value}-${index}-${getActionId(action, index)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleActions = computed(() => {
|
const visibleActions = computed(() => {
|
||||||
@@ -726,8 +741,7 @@ const multiSelectActions = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
function getDropdownValue(action: DropdownAction) {
|
function getDropdownValue(action: DropdownAction) {
|
||||||
const actionIndex = currentStageObj.value.actions.indexOf(action);
|
const actionId = getActionId(action);
|
||||||
const actionId = getActionId(action, actionIndex);
|
|
||||||
const visibleOptions = getVisibleDropdownOptions(action);
|
const visibleOptions = getVisibleDropdownOptions(action);
|
||||||
const currentValue = actionStates.value[actionId]?.value ?? action.defaultOption ?? 0;
|
const currentValue = actionStates.value[actionId]?.value ?? action.defaultOption ?? 0;
|
||||||
|
|
||||||
@@ -742,14 +756,12 @@ function getDropdownValue(action: DropdownAction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isActionSelected(action: Action): boolean {
|
function isActionSelected(action: Action): boolean {
|
||||||
const actionIndex = currentStageObj.value.actions.indexOf(action);
|
const actionId = getActionId(action);
|
||||||
const actionId = getActionId(action, actionIndex);
|
|
||||||
return actionStates.value[actionId]?.selected || false;
|
return actionStates.value[actionId]?.selected || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAction(action: Action) {
|
function toggleAction(action: Action) {
|
||||||
const actionIndex = currentStageObj.value.actions.indexOf(action);
|
const actionId = getActionId(action);
|
||||||
const actionId = getActionId(action, actionIndex);
|
|
||||||
const state = actionStates.value[actionId];
|
const state = actionStates.value[actionId];
|
||||||
if (state) {
|
if (state) {
|
||||||
state.selected = !state.selected;
|
state.selected = !state.selected;
|
||||||
@@ -758,8 +770,7 @@ function toggleAction(action: Action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectDropdownOption(action: DropdownAction, selected: any) {
|
function selectDropdownOption(action: DropdownAction, selected: any) {
|
||||||
const actionIndex = currentStageObj.value.actions.indexOf(action);
|
const actionId = getActionId(action);
|
||||||
const actionId = getActionId(action, actionIndex);
|
|
||||||
const state = actionStates.value[actionId];
|
const state = actionStates.value[actionId];
|
||||||
if (state && selected !== undefined && selected !== null) {
|
if (state && selected !== undefined && selected !== null) {
|
||||||
const optionIndex = action.options.findIndex(
|
const optionIndex = action.options.findIndex(
|
||||||
@@ -775,8 +786,7 @@ function selectDropdownOption(action: DropdownAction, selected: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isChipSelected(action: MultiSelectChipsAction, optionIndex: number): boolean {
|
function isChipSelected(action: MultiSelectChipsAction, optionIndex: number): boolean {
|
||||||
const actionIndex = currentStageObj.value.actions.indexOf(action);
|
const actionId = getActionId(action);
|
||||||
const actionId = getActionId(action, actionIndex);
|
|
||||||
const selectedSet = actionStates.value[actionId]?.value as Set<number> | undefined;
|
const selectedSet = actionStates.value[actionId]?.value as Set<number> | undefined;
|
||||||
|
|
||||||
const visibleOptions = getVisibleMultiSelectOptions(action);
|
const visibleOptions = getVisibleMultiSelectOptions(action);
|
||||||
@@ -787,8 +797,7 @@ function isChipSelected(action: MultiSelectChipsAction, optionIndex: number): bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleChip(action: MultiSelectChipsAction, optionIndex: number) {
|
function toggleChip(action: MultiSelectChipsAction, optionIndex: number) {
|
||||||
const actionIndex = currentStageObj.value.actions.indexOf(action);
|
const actionId = getActionId(action);
|
||||||
const actionId = getActionId(action, actionIndex);
|
|
||||||
const state = actionStates.value[actionId];
|
const state = actionStates.value[actionId];
|
||||||
if (state && state.value instanceof Set) {
|
if (state && state.value instanceof Set) {
|
||||||
const visibleOptions = getVisibleMultiSelectOptions(action);
|
const visibleOptions = getVisibleMultiSelectOptions(action);
|
||||||
@@ -814,31 +823,6 @@ const isAnyVisibleInputs = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getModpackFilesFromStorage(): {
|
|
||||||
interactive: ModerationModpackItem[];
|
|
||||||
permanentNo: ModerationModpackItem[];
|
|
||||||
} {
|
|
||||||
try {
|
|
||||||
const sessionData = sessionStorage.getItem(`modpack-permissions-data-${props.project.id}`);
|
|
||||||
const interactive = sessionData ? (JSON.parse(sessionData) as ModerationModpackItem[]) : [];
|
|
||||||
|
|
||||||
const permanentNoData = sessionStorage.getItem(
|
|
||||||
`modpack-permissions-permanent-no-${props.project.id}`,
|
|
||||||
);
|
|
||||||
const permanentNo = permanentNoData
|
|
||||||
? (JSON.parse(permanentNoData) as ModerationModpackItem[])
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
interactive: interactive || [],
|
|
||||||
permanentNo: permanentNo || [],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to parse session storage modpack data:", error);
|
|
||||||
return { interactive: [], permanentNo: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assembleFullMessage() {
|
async function assembleFullMessage() {
|
||||||
const messageParts: MessagePart[] = [];
|
const messageParts: MessagePart[] = [];
|
||||||
|
|
||||||
@@ -1061,7 +1045,7 @@ function nextStage() {
|
|||||||
if (isModpackPermissionsStage.value && !modpackPermissionsComplete.value) {
|
if (isModpackPermissionsStage.value && !modpackPermissionsComplete.value) {
|
||||||
addNotification({
|
addNotification({
|
||||||
title: "Modpack permissions stage unfinished",
|
title: "Modpack permissions stage unfinished",
|
||||||
text: "Please complete the modpack permissions stage before proceeding.",
|
message: "Please complete the modpack permissions stage before proceeding.",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1108,14 +1092,13 @@ async function generateMessage() {
|
|||||||
const baseMessage = await assembleFullMessage();
|
const baseMessage = await assembleFullMessage();
|
||||||
let fullMessage = baseMessage;
|
let fullMessage = baseMessage;
|
||||||
|
|
||||||
if (props.project.project_type === "modpack") {
|
if (
|
||||||
const modpackFilesData = getModpackFilesFromStorage();
|
props.project.project_type === "modpack" &&
|
||||||
|
Object.keys(modpackJudgements.value).length > 0
|
||||||
if (modpackFilesData.interactive.length > 0 || modpackFilesData.permanentNo.length > 0) {
|
) {
|
||||||
const modpackMessage = generateModpackMessage(modpackFilesData);
|
const modpackMessage = generateModpackMessage(modpackJudgements.value);
|
||||||
if (modpackMessage) {
|
if (modpackMessage) {
|
||||||
fullMessage = baseMessage ? `${baseMessage}\n\n${modpackMessage}` : modpackMessage;
|
fullMessage = baseMessage ? `${baseMessage}\n\n${modpackMessage}` : modpackMessage;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1138,7 +1121,7 @@ async function generateMessage() {
|
|||||||
console.error("Error generating message:", error);
|
console.error("Error generating message:", error);
|
||||||
addNotification({
|
addNotification({
|
||||||
title: "Error generating message",
|
title: "Error generating message",
|
||||||
text: "Failed to generate moderation message. Please try again.",
|
message: "Failed to generate moderation message. Please try again.",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1146,34 +1129,25 @@ async function generateMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateModpackMessage(allFiles: {
|
function generateModpackMessage(judgements: ModerationJudgements) {
|
||||||
interactive: ModerationModpackItem[];
|
|
||||||
permanentNo: ModerationModpackItem[];
|
|
||||||
}) {
|
|
||||||
const issues = [];
|
const issues = [];
|
||||||
|
|
||||||
const attributeMods: string[] = [];
|
const attributeMods = [];
|
||||||
const noMods: string[] = [];
|
const noMods = [];
|
||||||
const permanentNoMods: string[] = [];
|
const permanentNoMods = [];
|
||||||
const unidentifiedMods: string[] = [];
|
const unidentifiedMods = [];
|
||||||
|
|
||||||
allFiles.interactive.forEach((file) => {
|
for (const [, judgement] of Object.entries(judgements)) {
|
||||||
if (file.status === "unidentified") {
|
if (judgement.status === "with-attribution") {
|
||||||
if (file.approved === "no") {
|
attributeMods.push(judgement.file_name);
|
||||||
unidentifiedMods.push(file.file_name);
|
} else if (judgement.status === "no") {
|
||||||
}
|
noMods.push(judgement.file_name);
|
||||||
} else if (file.status === "with-attribution" && file.approved === "no") {
|
} else if (judgement.status === "permanent-no") {
|
||||||
attributeMods.push(file.file_name);
|
permanentNoMods.push(judgement.file_name);
|
||||||
} else if (file.status === "no" && file.approved === "no") {
|
} else if (judgement.status === "unidentified") {
|
||||||
noMods.push(file.file_name);
|
unidentifiedMods.push(judgement.file_name);
|
||||||
} else if (file.status === "permanent-no") {
|
|
||||||
permanentNoMods.push(file.file_name);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
allFiles.permanentNo.forEach((file) => {
|
|
||||||
permanentNoMods.push(file.file_name);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
attributeMods.length > 0 ||
|
attributeMods.length > 0 ||
|
||||||
@@ -1183,12 +1157,6 @@ function generateModpackMessage(allFiles: {
|
|||||||
) {
|
) {
|
||||||
issues.push("## Copyrighted content");
|
issues.push("## Copyrighted content");
|
||||||
|
|
||||||
if (unidentifiedMods.length > 0) {
|
|
||||||
issues.push(
|
|
||||||
`${finalPermissionMessages.unidentified}\n${unidentifiedMods.map((mod) => `- ${mod}`).join("\n")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attributeMods.length > 0) {
|
if (attributeMods.length > 0) {
|
||||||
issues.push(
|
issues.push(
|
||||||
`${finalPermissionMessages["with-attribution"]}\n${attributeMods.map((mod) => `- ${mod}`).join("\n")}`,
|
`${finalPermissionMessages["with-attribution"]}\n${attributeMods.map((mod) => `- ${mod}`).join("\n")}`,
|
||||||
@@ -1204,13 +1172,18 @@ function generateModpackMessage(allFiles: {
|
|||||||
`${finalPermissionMessages["permanent-no"]}\n${permanentNoMods.map((mod) => `- ${mod}`).join("\n")}`,
|
`${finalPermissionMessages["permanent-no"]}\n${permanentNoMods.map((mod) => `- ${mod}`).join("\n")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (unidentifiedMods.length > 0) {
|
||||||
|
issues.push(
|
||||||
|
`${finalPermissionMessages["unidentified"]}\n${unidentifiedMods.map((mod) => `- ${mod}`).join("\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues.join("\n\n");
|
return issues.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasNextProject = ref(false);
|
async function sendMessage(status: "approved" | "rejected" | "withheld") {
|
||||||
async function sendMessage(status: ProjectStatus) {
|
|
||||||
try {
|
try {
|
||||||
await useBaseFetch(`project/${props.project.id}`, {
|
await useBaseFetch(`project/${props.project.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
@@ -1244,73 +1217,55 @@ async function sendMessage(status: ProjectStatus) {
|
|||||||
|
|
||||||
done.value = true;
|
done.value = true;
|
||||||
|
|
||||||
hasNextProject.value = await moderationStore.completeCurrentProject(
|
// Clear local storage for future reviews
|
||||||
props.project.id,
|
localStorage.removeItem(`modpack-permissions-${props.project.id}`);
|
||||||
"completed",
|
localStorage.removeItem(`modpack-permissions-index-${props.project.id}`);
|
||||||
);
|
localStorage.removeItem(`moderation-actions-${props.project.slug}`);
|
||||||
|
localStorage.removeItem(`moderation-inputs-${props.project.slug}`);
|
||||||
|
actionStates.value = {};
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
title: "Moderation submitted",
|
||||||
|
message: `Project ${status} successfully.`,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting moderation:", error);
|
console.error("Error submitting moderation:", error);
|
||||||
addNotification({
|
addNotification({
|
||||||
title: "Error submitting moderation",
|
title: "Error submitting moderation",
|
||||||
text: "Failed to submit moderation decision. Please try again.",
|
message: "Failed to submit moderation decision. Please try again.",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function endChecklist(status?: string) {
|
async function goToNextProject() {
|
||||||
clearProjectLocalStorage();
|
const currentIds = JSON.parse(localStorage.getItem("moderation-future-projects") || "[]");
|
||||||
|
|
||||||
if (!hasNextProject.value) {
|
if (currentIds.length === 0) {
|
||||||
await navigateTo({
|
await navigateTo("/moderation/review");
|
||||||
name: "moderation",
|
return;
|
||||||
state: {
|
|
||||||
confetti: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
if (moderationStore.currentQueue.total > 1) {
|
|
||||||
addNotification({
|
|
||||||
title: "Moderation completed",
|
|
||||||
text: `You have completed the moderation queue.`,
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
addNotification({
|
|
||||||
title: "Moderation submitted",
|
|
||||||
text: `Project ${status ?? "completed successfully"}.`,
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
navigateTo({
|
|
||||||
name: "type-id",
|
|
||||||
params: {
|
|
||||||
type: "project",
|
|
||||||
id: moderationStore.getCurrentProjectId(),
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
showChecklist: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextProjectId = currentIds[0];
|
||||||
|
const remainingIds = currentIds.slice(1);
|
||||||
|
|
||||||
|
localStorage.setItem("moderation-future-projects", JSON.stringify(remainingIds));
|
||||||
|
|
||||||
|
await router.push({
|
||||||
|
name: "type-id",
|
||||||
|
params: {
|
||||||
|
type: "project",
|
||||||
|
id: nextProjectId,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
showChecklist: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function skipCurrentProject() {
|
async function exitModeration() {
|
||||||
hasNextProject.value = await moderationStore.completeCurrentProject(props.project.id, "skipped");
|
await navigateTo("/moderation/review");
|
||||||
|
|
||||||
await endChecklist("skipped");
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearProjectLocalStorage() {
|
|
||||||
localStorage.removeItem(`modpack-permissions-${props.project.id}`);
|
|
||||||
localStorage.removeItem(`modpack-permissions-index-${props.project.id}`);
|
|
||||||
localStorage.removeItem(`moderation-actions-${props.project.slug}`);
|
|
||||||
localStorage.removeItem(`moderation-inputs-${props.project.slug}`);
|
|
||||||
localStorage.removeItem(`moderation-stage-${props.project.slug}`);
|
|
||||||
actionStates.value = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLastVisibleStage = computed(() => {
|
const isLastVisibleStage = computed(() => {
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-if="moderation">
|
|
||||||
<Chips v-model="reasonFilter" :items="reasons" />
|
|
||||||
<p v-if="reports.length === MAX_REPORTS" class="text-red">
|
|
||||||
There are at least {{ MAX_REPORTS }} open reports. This page is at its max reports and will
|
|
||||||
not show any more recent ones.
|
|
||||||
</p>
|
|
||||||
<p v-else-if="reasonFilter === 'All'">There are {{ filteredReports.length }} open reports.</p>
|
|
||||||
<p v-else>
|
|
||||||
There are {{ filteredReports.length }}/{{ reports.length }} open '{{ reasonFilter }}' reports.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<ReportInfo
|
<ReportInfo
|
||||||
v-for="report in filteredReports"
|
v-for="report in reports.filter(
|
||||||
|
(x) =>
|
||||||
|
(moderation || x.reporterUser.id === auth.user.id) &&
|
||||||
|
(viewMode === 'open' ? x.open : !x.open),
|
||||||
|
)"
|
||||||
:key="report.id"
|
:key="report.id"
|
||||||
:report="report"
|
:report="report"
|
||||||
:thread="report.thread"
|
:thread="report.thread"
|
||||||
:show-message="false"
|
|
||||||
:moderation="moderation"
|
:moderation="moderation"
|
||||||
raised
|
raised
|
||||||
:auth="auth"
|
:auth="auth"
|
||||||
@@ -24,12 +16,11 @@
|
|||||||
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
<p v-if="reports.length === 0">You don't have any active reports.</p>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Chips } from "@modrinth/ui";
|
|
||||||
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
import ReportInfo from "~/components/ui/report/ReportInfo.vue";
|
||||||
import { addReportMessage } from "~/helpers/threads.js";
|
import { addReportMessage } from "~/helpers/threads.js";
|
||||||
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
import { asEncodedJsonArray, fetchSegmented } from "~/utils/fetch-helpers.ts";
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
moderation: {
|
moderation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -41,14 +32,9 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const viewMode = ref("open");
|
const viewMode = ref("open");
|
||||||
const reasonFilter = ref("All");
|
|
||||||
const reports = ref([]);
|
const reports = ref([]);
|
||||||
|
|
||||||
const MAX_REPORTS = 1500;
|
let { data: rawReports } = await useAsyncData("report", () => useBaseFetch("report?count=1000"));
|
||||||
|
|
||||||
let { data: rawReports } = await useAsyncData("report", () =>
|
|
||||||
useBaseFetch(`report?count=${MAX_REPORTS}`),
|
|
||||||
);
|
|
||||||
|
|
||||||
rawReports = rawReports.value.map((report) => {
|
rawReports = rawReports.value.map((report) => {
|
||||||
report.item_id = report.item_id.replace(/"/g, "");
|
report.item_id = report.item_id.replace(/"/g, "");
|
||||||
@@ -65,7 +51,6 @@ const userIds = [...new Set(reporterUsers.concat(reportedUsers))];
|
|||||||
const threadIds = [
|
const threadIds = [
|
||||||
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
...new Set(rawReports.filter((report) => report.thread_id).map((report) => report.thread_id)),
|
||||||
];
|
];
|
||||||
const reasons = ["All", ...new Set(rawReports.map((report) => report.report_type))];
|
|
||||||
|
|
||||||
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
const [{ data: users }, { data: versions }, { data: threads }] = await Promise.all([
|
||||||
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
await useAsyncData(`users?ids=${JSON.stringify(userIds)}`, () =>
|
||||||
@@ -108,13 +93,4 @@ reports.value = rawReports.map((report) => {
|
|||||||
report.open = true;
|
report.open = true;
|
||||||
return report;
|
return report;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredReports = computed(() =>
|
|
||||||
reports.value?.filter(
|
|
||||||
(x) =>
|
|
||||||
(props.moderation || x.reporterUser.id === props.auth.user.id) &&
|
|
||||||
(viewMode.value === "open" ? x.open : !x.open) &&
|
|
||||||
(reasonFilter.value === "All" || reasonFilter.value === x.report_type),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
Loaded 100 of 321 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user