mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
Compare commits
25
Commits
v0.17.6
...
14b2943dbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14b2943dbe | ||
|
|
5b16a774f9 | ||
|
|
b0ec93f8ab | ||
|
|
2070428080 | ||
|
|
9582c1a6fd | ||
|
|
4897c2da94 | ||
|
|
457254f210 | ||
|
|
283f38c936 | ||
|
|
1dba3bfee8 | ||
|
|
37a5e14657 | ||
|
|
cc66d80165 | ||
|
|
9628b4c269 | ||
|
|
99683f570a | ||
|
|
762d9fe4f4 | ||
|
|
f948b5d358 | ||
|
|
4dbea80e65 | ||
|
|
74c672df49 | ||
|
|
3597bb4821 | ||
|
|
cf50e28dda | ||
|
|
4ca48e4589 | ||
|
|
17f13097f2 | ||
|
|
a9de420d57 | ||
|
|
ab7359a4c5 | ||
|
|
97b5138672 | ||
|
|
579109a478 |
@@ -10,6 +10,7 @@ on:
|
||||
- 'packages/ui/**/*'
|
||||
- 'packages/utils/**/*'
|
||||
- 'packages/assets/**/*'
|
||||
- 'packages/moderation/**/*'
|
||||
- '**/wrangler.jsonc'
|
||||
- '**/pnpm-*.yaml'
|
||||
- '.github/workflows/frontend-deploy.yml'
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- 'packages/ui/**/*'
|
||||
- 'packages/utils/**/*'
|
||||
- 'packages/assets/**/*'
|
||||
- 'packages/moderation/**/*'
|
||||
- '**/wrangler.jsonc'
|
||||
- '**/pnpm-*.yaml'
|
||||
- '.github/workflows/frontend-preview.yml'
|
||||
|
||||
@@ -59,6 +59,14 @@ apps/frontend/src/generated
|
||||
.turbo
|
||||
target
|
||||
generated
|
||||
!apps/app-frontend/src/generated/
|
||||
!apps/app-frontend/src/generated/app-events/
|
||||
!apps/app-frontend/src/generated/app-events/*.ts
|
||||
!apps/app-frontend/src/generated/app-events/README.md
|
||||
!apps/app-frontend/src/generated/app-events/postcard/
|
||||
!apps/app-frontend/src/generated/app-events/postcard/index.d.ts
|
||||
!apps/app-frontend/src/generated/app-events/postcard/index.js
|
||||
!apps/app-frontend/src/generated/app-events/postcard/package.json
|
||||
.env
|
||||
|
||||
# app testing dir
|
||||
|
||||
Generated
+172
-2
@@ -2116,7 +2116,7 @@ checksum = "ff6669899e23cb87b43daf7996f0ea3b9c07d0fb933d745bb7b815b052515ae3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"serde_derive_internals 0.29.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
@@ -2340,6 +2340,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
@@ -3991,6 +4000,28 @@ dependencies = [
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "genco"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ab846431e5d637791b3279e7242fe2b21e11c3d8b4cf6a99f645c5f16ba7c0"
|
||||
dependencies = [
|
||||
"genco-macros",
|
||||
"relative-path",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "genco-macros"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42a1fe5a699c7f1d36ea6e04ed680a5c787cabff4b610ae3b8954ea3bcefec1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.9"
|
||||
@@ -7509,6 +7540,45 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard-bindgen"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e709af61573ae2563a00e760ff7db47f5c70b9d96c6dcb28993526fb8479f68"
|
||||
dependencies = [
|
||||
"postcard-bindgen-core",
|
||||
"postcard-bindgen-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard-bindgen-core"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f19cebcb145fdccb24b63ad410785aa7c2ff9364551646ca7a927422d2d0386"
|
||||
dependencies = [
|
||||
"convert_case 0.11.0",
|
||||
"genco",
|
||||
"tree-ds",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard-bindgen-derive"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0372f55dd7c15d73f7b0f7ca507ac3db5653f346b2b44a36537e2ad39f55441b"
|
||||
dependencies = [
|
||||
"convert_case 0.11.0",
|
||||
"genco",
|
||||
"postcard-bindgen-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"regex-macro",
|
||||
"serde",
|
||||
"serde_derive_internals 0.30.0",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
@@ -8391,12 +8461,27 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
|
||||
|
||||
[[package]]
|
||||
name = "regex-macro"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d306632607af6ec61c0b117971d57a96381b6317cf18ae419b5558048fe016e"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.2"
|
||||
@@ -9062,7 +9147,7 @@ checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"serde_derive_internals 0.29.1",
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
@@ -9305,6 +9390,16 @@ dependencies = [
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sequential_gen"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d630b1418311e028ceb45e1e567845501a9d9c770a258b80d2edc025ffad17c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -9410,6 +9505,17 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_ini"
|
||||
version = "0.2.0"
|
||||
@@ -9850,6 +9956,9 @@ name = "spin"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spinning_top"
|
||||
@@ -10277,6 +10386,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -10890,6 +11010,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "testcontainers"
|
||||
version = "0.25.2"
|
||||
@@ -10979,6 +11108,8 @@ dependencies = [
|
||||
"path-util",
|
||||
"phf 0.13.1",
|
||||
"png 0.18.0",
|
||||
"postcard",
|
||||
"postcard-bindgen",
|
||||
"quartz_nbt",
|
||||
"quick-xml 0.38.3",
|
||||
"rand 0.8.5",
|
||||
@@ -10986,6 +11117,7 @@ dependencies = [
|
||||
"reqwest 0.12.24",
|
||||
"rgb",
|
||||
"serde",
|
||||
"serde-binhum",
|
||||
"serde_ini",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
@@ -11003,6 +11135,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"ts-rs",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"uuid 1.23.3",
|
||||
@@ -11679,12 +11812,49 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-ds"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "710c388a2bb52d1a7186ae4aa2e92c6fae0606426bf4bdfb4106fa1e8e5c602e"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"sequential_gen",
|
||||
"serde",
|
||||
"spin 0.10.0",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"thiserror 2.0.17",
|
||||
"ts-rs-macros",
|
||||
"uuid 1.23.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-rs-macros"
|
||||
version = "12.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.27.0"
|
||||
|
||||
@@ -149,6 +149,7 @@ path-util = { path = "packages/path-util" }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
png = "0.18.0"
|
||||
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
|
||||
postcard-bindgen = "0.8.0"
|
||||
proc-macro2 = { version = "1.0" }
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
@@ -232,6 +233,7 @@ tracing-actix-web = { version = "0.7.19", default-features = false }
|
||||
tracing-ecs = "0.5.0"
|
||||
tracing-error = "0.2.1"
|
||||
tracing-subscriber = "0.3.20"
|
||||
ts-rs = "12.0.1"
|
||||
typed-path = "0.12.0"
|
||||
url = "2.5.7"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
@@ -24,3 +24,7 @@ gam = "gam"
|
||||
consts = "consts"
|
||||
# short for "Copy"
|
||||
Cpy = "Cpy"
|
||||
|
||||
[default.extend-identifiers]
|
||||
# Constant from the `zip` crate
|
||||
ZIP64_BYTES_THR = "ZIP64_BYTES_THR"
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
*.gltf
|
||||
src/locales/
|
||||
src/assets/**/*.svg
|
||||
|
||||
# Generated app-event bindings
|
||||
src/generated/app-events/*.ts
|
||||
src/generated/app-events/postcard/**
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config
|
||||
|
||||
export default config.append([
|
||||
{
|
||||
ignores: ['src/generated/app-events/*.ts', 'src/generated/app-events/postcard/**'],
|
||||
},
|
||||
])
|
||||
|
||||
@@ -90,9 +90,9 @@ import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||
import SurveyPopup from '@/components/ui/SurveyPopup.vue'
|
||||
import WindowControls from '@/components/ui/WindowControls.vue'
|
||||
import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { config } from '@/config'
|
||||
import {
|
||||
ads_consent_listener,
|
||||
hide_ads_window,
|
||||
init_ads_window,
|
||||
perform_ads_consent_action,
|
||||
@@ -103,7 +103,6 @@ import {
|
||||
import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics'
|
||||
import { check_reachable } from '@/helpers/auth.js'
|
||||
import { get_user, get_version } from '@/helpers/cache.js'
|
||||
import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js'
|
||||
import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install'
|
||||
import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance'
|
||||
import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts'
|
||||
@@ -142,6 +141,7 @@ import {
|
||||
} from '@/providers/download-progress.ts'
|
||||
import { createServerInstall, provideServerInstall } from '@/providers/server-install'
|
||||
import { setupProviders } from '@/providers/setup'
|
||||
import { setupAppEventsProvider } from '@/providers/setup/app-events'
|
||||
import { setupAuthProvider } from '@/providers/setup/auth'
|
||||
import { setupLoadingStateProvider } from '@/providers/setup/loading-state'
|
||||
import { useError } from '@/store/error.js'
|
||||
@@ -157,6 +157,7 @@ import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal'
|
||||
const themeStore = useTheming()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider()
|
||||
const breadcrumbManager = createBreadcrumbManager()
|
||||
provideBreadcrumbManager(breadcrumbManager)
|
||||
const canNavigateBack = ref(false)
|
||||
@@ -243,11 +244,22 @@ const notificationManager = new AppNotificationManager()
|
||||
provideNotificationManager(notificationManager)
|
||||
const { handleError, addNotification } = notificationManager
|
||||
|
||||
useAppEvent(
|
||||
'warning',
|
||||
(event) =>
|
||||
addNotification({
|
||||
title: 'Warning',
|
||||
text: event.message,
|
||||
type: 'warning',
|
||||
}),
|
||||
appEvents,
|
||||
)
|
||||
|
||||
const popupNotificationManager = new AppPopupNotificationManager()
|
||||
providePopupNotificationManager(popupNotificationManager)
|
||||
const { addPopupNotification } = popupNotificationManager
|
||||
let adsConsentPopupId = null
|
||||
let unlistenAdsConsent
|
||||
useAppEvent('ads_consent_required', handleAdsConsentRequired, appEvents)
|
||||
|
||||
const appVersion = getVersion()
|
||||
const tauriApiClient = new TauriModrinthClient({
|
||||
@@ -384,7 +396,6 @@ const authUnreachable = computed(() => {
|
||||
onMounted(async () => {
|
||||
await useCheckDisableMouseover()
|
||||
try {
|
||||
unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired)
|
||||
handleAdsConsentRequired(await should_show_ads_consent_popup())
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
@@ -407,7 +418,6 @@ onUnmounted(async () => {
|
||||
fullscreenAdsWindowHold = false
|
||||
await release_ads_window_hold().catch(handleError)
|
||||
}
|
||||
await unlistenAdsConsent?.()
|
||||
await unlistenUpdateDownload?.()
|
||||
})
|
||||
|
||||
@@ -616,14 +626,6 @@ async function setupApp() {
|
||||
document.getElementsByTagName('html')[0].classList.add('windows')
|
||||
}
|
||||
|
||||
await warning_listener((e) =>
|
||||
addNotification({
|
||||
title: 'Warning',
|
||||
text: e.message,
|
||||
type: 'warning',
|
||||
}),
|
||||
)
|
||||
|
||||
fetch(`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
@@ -672,7 +674,7 @@ async function setupApp() {
|
||||
}
|
||||
|
||||
const stateFailed = ref(false)
|
||||
initialize_state()
|
||||
initialize_state(appEventChannel)
|
||||
.then(() => {
|
||||
setupApp().catch((err) => {
|
||||
stateFailed.value = true
|
||||
@@ -802,7 +804,7 @@ const errorModal = ref()
|
||||
const minecraftAuthErrorModal = ref()
|
||||
const minecraftRequiredModal = ref()
|
||||
|
||||
const contentInstall = createContentInstall({ router, handleError })
|
||||
const contentInstall = createContentInstall({ router, handleError, appEvents })
|
||||
provideContentInstall(contentInstall)
|
||||
const {
|
||||
instances: contentInstallInstances,
|
||||
@@ -835,7 +837,12 @@ const {
|
||||
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
|
||||
} = contentInstall
|
||||
|
||||
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
|
||||
const serverInstall = createServerInstall({
|
||||
router,
|
||||
handleError,
|
||||
popupNotificationManager,
|
||||
appEvents,
|
||||
})
|
||||
provideServerInstall(serverInstall)
|
||||
const {
|
||||
setInstallToPlayModal: setServerInstallToPlayModal,
|
||||
@@ -1011,8 +1018,8 @@ onMounted(() => {
|
||||
const accounts = ref(null)
|
||||
provide('accountsCard', accounts)
|
||||
|
||||
command_listener(handleCommand)
|
||||
notification_listener(handleLiveNotification)
|
||||
useAppEvent('command', handleCommand, appEvents)
|
||||
useAppEvent('notification', handleLiveNotification, appEvents)
|
||||
|
||||
async function markLiveNotificationRead(notification) {
|
||||
try {
|
||||
@@ -1438,6 +1445,7 @@ async function downloadUpdate(versionToDownload) {
|
||||
handleError(e)
|
||||
})
|
||||
unlistenUpdateDownload = await subscribeToDownloadProgress(
|
||||
appEvents,
|
||||
appUpdateDownload,
|
||||
versionToDownload.version,
|
||||
)
|
||||
|
||||
@@ -104,8 +104,9 @@ import {
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import {
|
||||
get_default_user,
|
||||
@@ -114,7 +115,6 @@ import {
|
||||
set_default_user,
|
||||
users,
|
||||
} from '@/helpers/auth'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import type { Skin } from '@/helpers/skins'
|
||||
import { get_available_skins } from '@/helpers/skins'
|
||||
@@ -251,16 +251,12 @@ async function logout(id: string) {
|
||||
trackEvent('AccountLogOut')
|
||||
}
|
||||
|
||||
const unlisten = await process_listener(async (e) => {
|
||||
useAppEvent('process', async (e) => {
|
||||
if (e.event === 'launched') {
|
||||
await refreshValues()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlisten()
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
notSignedIn: {
|
||||
id: 'minecraft-account.not-signed-in',
|
||||
|
||||
@@ -148,8 +148,8 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import AppUpdateButton from '@/components/ui/app-update-button/index.vue'
|
||||
import { useInstallJobNotifications } from '@/composables/browse/install-job-notifications'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { loading_listener, process_listener } from '@/helpers/events'
|
||||
import { get_many as getInstances } from '@/helpers/instance'
|
||||
import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process'
|
||||
import type { LoadingBar } from '@/helpers/state'
|
||||
@@ -294,7 +294,7 @@ onMounted(() => {
|
||||
window.addEventListener('online', handleOnline)
|
||||
})
|
||||
|
||||
const unlistenProcess = await process_listener(async () => {
|
||||
useAppEvent('process', async () => {
|
||||
await refresh()
|
||||
})
|
||||
|
||||
@@ -554,7 +554,7 @@ const installJobNotifications = await useInstallJobNotifications({
|
||||
|
||||
await refreshLoadingBars()
|
||||
|
||||
const unlistenLoading = await loading_listener(async () => {
|
||||
useAppEvent('loading', async () => {
|
||||
await refreshLoadingBars()
|
||||
})
|
||||
|
||||
@@ -573,8 +573,6 @@ onBeforeUnmount(() => {
|
||||
dismissed.value = false
|
||||
window.removeEventListener('offline', handleOffline)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
unlistenProcess()
|
||||
unlistenLoading()
|
||||
installJobNotifications.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup>
|
||||
import { XIcon } from '@modrinth/assets'
|
||||
import { FolderOpenIcon, XIcon } from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
FileTreeSelect,
|
||||
injectNotificationManager,
|
||||
injectPopupNotificationManager,
|
||||
NewModal,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
@@ -15,8 +16,10 @@ import { ref, shallowRef } from 'vue'
|
||||
|
||||
import { PackageIcon } from '@/assets/icons'
|
||||
import { export_instance_mrpack, get_pack_export_candidates } from '@/helpers/instance'
|
||||
import { highlightInFolder } from '@/helpers/utils'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const popupNotificationManager = injectPopupNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -39,6 +42,14 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Enter modpack description...',
|
||||
},
|
||||
exportButton: { id: 'app.export-modal.export-button', defaultMessage: 'Export' },
|
||||
exportComplete: {
|
||||
id: 'app.export-modal.export-complete',
|
||||
defaultMessage: 'Export complete',
|
||||
},
|
||||
exportCompleteDescription: {
|
||||
id: 'app.export-modal.export-complete-description',
|
||||
defaultMessage: '{name} was exported successfully.',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -93,16 +104,35 @@ const exportPack = async () => {
|
||||
})
|
||||
|
||||
if (outputPath) {
|
||||
export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
includedFilePaths.value,
|
||||
excludedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
).catch((err) => handleError(err))
|
||||
exportModal.value.hide()
|
||||
|
||||
try {
|
||||
await export_instance_mrpack(
|
||||
props.instance.id,
|
||||
outputPath,
|
||||
includedFilePaths.value,
|
||||
excludedFilePaths.value,
|
||||
versionInput.value,
|
||||
exportDescription.value,
|
||||
nameInput.value,
|
||||
)
|
||||
|
||||
const fileName = outputPath.split(/[\\/]/).pop() ?? outputPath
|
||||
popupNotificationManager.addPopupNotification({
|
||||
title: formatMessage(messages.exportComplete),
|
||||
text: formatMessage(messages.exportCompleteDescription, { name: fileName }),
|
||||
type: 'success',
|
||||
buttons: [
|
||||
{
|
||||
label: formatMessage(commonMessages.openInFolderButton),
|
||||
icon: FolderOpenIcon,
|
||||
action: () => highlightInFolder(outputPath).catch(handleError),
|
||||
},
|
||||
],
|
||||
})
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
import { Avatar, IconButton, injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
@@ -136,7 +136,7 @@ defineExpose({
|
||||
|
||||
const currentEvent = ref(null)
|
||||
|
||||
const unlisten = await process_listener((e) => {
|
||||
useAppEvent('process', (e) => {
|
||||
if (e.instance_id === props.instance.id) {
|
||||
currentEvent.value = e.event
|
||||
if (e.event === 'finished') {
|
||||
@@ -148,7 +148,6 @@ const unlisten = await process_listener((e) => {
|
||||
onMounted(() => {
|
||||
checkProcess()
|
||||
})
|
||||
onUnmounted(() => unlisten())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -7,7 +7,7 @@ import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import NavButton from '@/components/ui/NavButton.vue'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { instanceKeys } from '@/pages/instance/query-options'
|
||||
|
||||
@@ -148,7 +148,7 @@ const getInstances = async () => {
|
||||
await getInstances()
|
||||
updateMaxAuto()
|
||||
|
||||
const unlistenInstance = await instance_listener(async (event) => {
|
||||
useAppEvent('instance', async (event) => {
|
||||
if (event.event !== 'synced') {
|
||||
await getInstances()
|
||||
}
|
||||
@@ -162,7 +162,6 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateMaxAuto)
|
||||
document.body.classList.remove('quick-instance-dragging')
|
||||
clearOverdragFlash()
|
||||
unlistenInstance()
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
@@ -82,7 +82,7 @@ import { injectLoadingState } from '@modrinth/ui'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue'
|
||||
import { loading_listener } from '@/helpers/events.js'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
|
||||
const doneLoading = ref(false)
|
||||
const loadingProgress = ref(0)
|
||||
@@ -132,13 +132,10 @@ function fakeLoadingIncrease() {
|
||||
}
|
||||
}
|
||||
|
||||
loading_listener(async (e) => {
|
||||
useAppEvent('loading', (e) => {
|
||||
if (e.event.type === 'directory_move') {
|
||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||
message.value = 'Updating app directory...'
|
||||
} else if (e.event.type === 'checking_for_updates') {
|
||||
loadingProgress.value = 100 * (e.fraction ?? 1)
|
||||
message.value = 'Checking for updates...'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -34,6 +34,7 @@ import { get_project_many, get_version, get_version_many } from '@/helpers/cache
|
||||
import { wait_for_install_job } from '@/helpers/install'
|
||||
import { update_managed_modrinth_version } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
import { injectServerInstall } from '@/providers/server-install'
|
||||
|
||||
type Dependency = Labrinth.Versions.v3.Dependency
|
||||
@@ -74,6 +75,7 @@ type ProjectInfo = {
|
||||
}
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const appEvents = injectAppEvents()
|
||||
const { startInstallingServer, stopInstallingServer } = injectServerInstall()
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
@@ -253,7 +255,7 @@ async function handleUpdate() {
|
||||
try {
|
||||
if (modpackVersionId.value && instance.value) {
|
||||
const job = await update_managed_modrinth_version(instance.value.id, modpackVersionId.value)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await wait_for_install_job(appEvents, job.job_id)
|
||||
await onUpdateComplete.value()
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from '@/helpers/install'
|
||||
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
|
||||
type UpdateCompleteCallback = () => void | Promise<void>
|
||||
|
||||
@@ -54,6 +55,7 @@ const instance = ref<GameInstance | null>(null)
|
||||
const preview = ref<SharedInstanceUpdatePreview | null>(null)
|
||||
const onComplete = ref<UpdateCompleteCallback>(() => {})
|
||||
const { formatMessage } = useVIntl()
|
||||
const appEvents = injectAppEvents()
|
||||
const { notifySharedInstanceError } = useSharedInstanceErrors()
|
||||
const diffs = computed<ContentDiffItem[]>(
|
||||
() =>
|
||||
@@ -78,7 +80,7 @@ async function update() {
|
||||
try {
|
||||
if (instance.value) {
|
||||
const job = await install_update_shared_instance(instance.value.id)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await wait_for_install_job(appEvents, job.job_id)
|
||||
await onComplete.value()
|
||||
successful = true
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ import {
|
||||
import { capitalizeString } from '@modrinth/utils'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project } from '@/helpers/cache'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
@@ -108,7 +108,7 @@ const stop = async (event: MouseEvent) => {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const unlistenProcesses = await process_listener(async () => {
|
||||
useAppEvent('process', async () => {
|
||||
await checkProcess()
|
||||
})
|
||||
|
||||
@@ -121,10 +121,6 @@ const checkProcess = async () => {
|
||||
onMounted(() => {
|
||||
checkProcess()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<SmartClickable>
|
||||
|
||||
@@ -5,12 +5,12 @@ import { GAME_MODES, injectNotificationManager } from '@modrinth/ui'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { instance_listener, process_listener } from '@/helpers/events'
|
||||
import { kill, run } from '@/helpers/instance'
|
||||
import { get_all } from '@/helpers/process'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
@@ -215,11 +215,11 @@ async function stopInstance(path: string) {
|
||||
const currentInstance = ref<string>()
|
||||
const currentWorld = ref<string>()
|
||||
|
||||
const unlistenProcesses = await process_listener(async () => {
|
||||
useAppEvent('process', async () => {
|
||||
await checkProcesses()
|
||||
})
|
||||
|
||||
const unlistenInstances = await instance_listener(async () => {
|
||||
useAppEvent('instance', async () => {
|
||||
await populateJumpBackIn().catch(() => {
|
||||
console.error('Failed to populate jump back in')
|
||||
})
|
||||
@@ -251,11 +251,6 @@ onMounted(() => {
|
||||
checkProcesses()
|
||||
linuxPopulateCount.value = 0
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses()
|
||||
unlistenInstances()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
import { install_job_listener } from '@/helpers/events'
|
||||
import {
|
||||
install_job_dismiss,
|
||||
install_job_list,
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
type InstallProgress,
|
||||
} from '@/helpers/install'
|
||||
import { get_many as getInstances } from '@/helpers/instance'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -234,6 +234,7 @@ export async function useInstallJobNotifications(opts: {
|
||||
handleError: (err: unknown) => void
|
||||
onChange: () => void
|
||||
}) {
|
||||
const appEvents = injectAppEvents()
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
const jobs = ref<InstallJobSnapshot[]>([])
|
||||
@@ -635,7 +636,7 @@ export async function useInstallJobNotifications(opts: {
|
||||
void refreshMetadata()
|
||||
}
|
||||
|
||||
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
|
||||
const unlisten = appEvents.on('install_job', applyJobUpdate)
|
||||
await refresh(false)
|
||||
|
||||
return {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
fetchCachedServerStatus,
|
||||
getFreshCachedServerStatus,
|
||||
} from '@/composables/instances/use-server-status-query'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { kill, list as listInstances } from '@/helpers/instance'
|
||||
import { get_by_instance_id } from '@/helpers/process'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
@@ -78,7 +78,6 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
|
||||
const contextMenuRef = ref<ContextMenuHandle | null>(null)
|
||||
let serverPingsActive = true
|
||||
let unlistenProcesses: (() => void) | null = null
|
||||
|
||||
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
|
||||
debugLog('checkServerRunningStates', { hitCount: hits.length })
|
||||
@@ -280,7 +279,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
process_listener((event: { event: string; instance_id: string }) => {
|
||||
useAppEvent('process', (event) => {
|
||||
debugLog('process event', event)
|
||||
if (event.event === 'finished') {
|
||||
const projectId = Object.entries(runningServerProjects.value).find(
|
||||
@@ -292,14 +291,9 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
unlistenProcesses = unlisten
|
||||
})
|
||||
.catch(options.handleError)
|
||||
|
||||
onUnmounted(() => {
|
||||
serverPingsActive = false
|
||||
unlistenProcesses?.()
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { onScopeDispose } from 'vue'
|
||||
|
||||
import {
|
||||
type AppEventHandler,
|
||||
type AppEvents,
|
||||
type AppEventType,
|
||||
injectAppEvents,
|
||||
} from '@/providers/app-events'
|
||||
|
||||
export function useAppEvent<Type extends AppEventType>(
|
||||
type: Type,
|
||||
handler: AppEventHandler<Type>,
|
||||
events: AppEvents = injectAppEvents(),
|
||||
) {
|
||||
const unsubscribe = events.on(type, handler)
|
||||
onScopeDispose(unsubscribe)
|
||||
return unsubscribe
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, type MaybeRefOrGetter, onUnmounted, toValue } from 'vue'
|
||||
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { toError } from '@/helpers/errors'
|
||||
import { friend_listener } from '@/helpers/events.js'
|
||||
import {
|
||||
acceptCachedFriend,
|
||||
add_friend,
|
||||
@@ -127,13 +127,9 @@ export function useFriends(options: {
|
||||
)
|
||||
}
|
||||
|
||||
let unlisten: (() => void) | undefined
|
||||
void friend_listener(() => {
|
||||
useAppEvent('friend', () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKey.value })
|
||||
}).then((listener) => {
|
||||
unlisten = listener
|
||||
})
|
||||
onUnmounted(() => unlisten?.())
|
||||
|
||||
return {
|
||||
query,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CommandPayload } from "./CommandPayload";
|
||||
import type { FriendPayload } from "./FriendPayload";
|
||||
import type { InstallJobSnapshot } from "./InstallJobSnapshot";
|
||||
import type { InstanceBulkUpdateProgressPayload } from "./InstanceBulkUpdateProgressPayload";
|
||||
import type { InstancePayload } from "./InstancePayload";
|
||||
import type { LoadingPayload } from "./LoadingPayload";
|
||||
import type { LogPayload } from "./LogPayload";
|
||||
import type { ProcessPayload } from "./ProcessPayload";
|
||||
import type { WarningPayload } from "./WarningPayload";
|
||||
|
||||
export type AppEvent = { "type": "loading", "payload": LoadingPayload } | { "type": "process", "payload": ProcessPayload } | { "type": "instance", "payload": InstancePayload } | { "type": "instance_bulk_update_progress", "payload": InstanceBulkUpdateProgressPayload } | { "type": "install_job", "payload": InstallJobSnapshot } | { "type": "command", "payload": CommandPayload } | { "type": "warning", "payload": WarningPayload } | { "type": "friend", "payload": FriendPayload } | { "type": "notification", "payload": unknown } | { "type": "log", "payload": LogPayload } | { "type": "ads_consent_required", "payload": boolean };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CommandPayload = { "event": "InstallMod", id: string, } | { "event": "InstallVersion", id: string, } | { "event": "InstallModpack", id: string, } | { "event": "InstallServer", id: string, } | { "event": "LaunchInstance", id: string, server: string | null, singleplayer_world: string | null, } | { "event": "InstallSharedInstanceInvite", invite_id: string, } | { "event": "RunMRPack", path: string, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FriendStatusPayload } from "./FriendStatusPayload";
|
||||
|
||||
export type FriendPayload = { "event": "friend_request", from: string, } | { "event": "user_offline", id: string, } | { "event": "status_update", user_status: FriendStatusPayload, } | { "event": "status_sync" };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FriendStatusPayload = { user_id: string, profile_name: string | null, last_update: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ImportLauncherType = "MultiMC" | "PrismLauncher" | "ATLauncher" | "GDLauncher" | "Curseforge" | "Unknown";
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallApiErrorDetails = { error: string, status?: number, method?: string, url?: string, route?: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallErrorContext = { operation: string, source_path?: string, target_path?: string, file_path?: string, entry_path?: string, urls: Array<string>, expected_hash?: string, expected_size?: number, project_id?: string, version_id?: string, minecraft_version?: string, loader?: string, java_version?: number, os?: string, arch?: string, };
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { InstallApiErrorDetails } from "./InstallApiErrorDetails";
|
||||
import type { InstallErrorContext } from "./InstallErrorContext";
|
||||
import type { InstallPhaseId } from "./InstallPhaseId";
|
||||
import type { SharedInstanceUnavailableReason } from "./SharedInstanceUnavailableReason";
|
||||
|
||||
export type InstallErrorView = { code: string, phase?: InstallPhaseId, message: string, reason?: SharedInstanceUnavailableReason, api?: InstallApiErrorDetails, context?: InstallErrorContext, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallJavaStep = "resolving" | "fetching_metadata" | "downloading" | "extracting" | "validating";
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallJobDisplay = { title: string, icon: string | null, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallJobKind = "create_instance" | "create_modpack_instance" | "create_shared_instance" | "import_instance" | "duplicate_instance" | "install_existing_instance" | "install_pack_to_existing_instance" | "update_shared_instance";
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { InstallErrorView } from "./InstallErrorView";
|
||||
import type { InstallJobDisplay } from "./InstallJobDisplay";
|
||||
import type { InstallJobKind } from "./InstallJobKind";
|
||||
import type { InstallJobStatus } from "./InstallJobStatus";
|
||||
import type { InstallPhaseDetails } from "./InstallPhaseDetails";
|
||||
import type { InstallPhaseId } from "./InstallPhaseId";
|
||||
import type { InstallProgress } from "./InstallProgress";
|
||||
import type { InstallTarget } from "./InstallTarget";
|
||||
|
||||
export type InstallJobSnapshot = { job_id: string, instance_id: string | null, kind: InstallJobKind, status: InstallJobStatus, target: InstallTarget, phase: InstallPhaseId, progress: InstallProgress | null, details: InstallPhaseDetails, display: InstallJobDisplay | null, error: InstallErrorView | null, rollback_error: InstallErrorView | null, created: string, modified: string, finished: string | null, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallJobStatus = "queued" | "running" | "succeeded" | "failed" | "interrupted" | "canceled";
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ImportLauncherType } from "./ImportLauncherType";
|
||||
import type { InstallJavaStep } from "./InstallJavaStep";
|
||||
import type { ModLoader } from "./ModLoader";
|
||||
|
||||
export type InstallPhaseDetails = { "type": "empty" } | { "type": "instance", name: string, } | { "type": "minecraft", game_version: string, loader: ModLoader, } | { "type": "java", major_version: number, step: InstallJavaStep, } | { "type": "modpack", project_id: string | null, version_id: string | null, title: string | null, } | { "type": "import", launcher_type: ImportLauncherType, instance_folder: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallPhaseId = "preparing_instance" | "resolving_pack" | "downloading_pack_file" | "reading_pack_manifest" | "downloading_content" | "extracting_overrides" | "resolving_minecraft" | "resolving_loader" | "preparing_java" | "downloading_minecraft" | "running_loader_processors" | "finalizing" | "rolling_back";
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { InstallProgressSecondary } from "./InstallProgressSecondary";
|
||||
|
||||
export type InstallProgress = { current: number, total: number, secondary?: InstallProgressSecondary, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallProgressSecondary = { current: number, total: number, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstallTarget = { "type": "new_instance", instance_id: string | null, } | { "type": "existing_instance", instance_id: string, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { InstanceBulkUpdateProgressStage } from "./InstanceBulkUpdateProgressStage";
|
||||
|
||||
export type InstanceBulkUpdateProgressPayload = { instanceId: string, stage: InstanceBulkUpdateProgressStage, current: number, total: number, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstanceBulkUpdateProgressStage = "resolving_versions" | "downloading" | "finishing";
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type InstancePayload = { instance_id: string, } & ({ "event": "created" } | { "event": "synced" } | { "event": "servers_updated" } | { "event": "world_updated", world: string, } | { "event": "server_joined", host: string, port: number, timestamp: string, } | { "event": "edited" } | { "event": "content_install_finished", project_ids: Array<string>, } | { "event": "content_install_failed", project_ids: Array<string>, message: string, } | { "event": "removed" });
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LoadingBarType = { "type": "legacy_data_migration" } | { "type": "directory_move", old: string, new: string, } | { "type": "java_download", version: number, } | { "type": "pack_file_download", instance_id: string, pack_name: string, icon: string | null, pack_version: string, } | { "type": "pack_download", instance_id: string, pack_name: string, icon: string | null, pack_id: string | null, pack_version: string | null, } | { "type": "pack_import", pack_name: string, } | { "type": "minecraft_download", instance_id: string, instance_name: string, } | { "type": "instance_update", instance_id: string, instance_name: string, } | { "type": "zip_extract", instance_id: string, instance_name: string, } | { "type": "pack_export", instance_id: string, instance_name: string, } | { "type": "config_change", new_path: string, } | { "type": "copy_instance", import_location: string, instance_name: string, } | { "type": "launcher_update", version: string, current_version: string, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LoadingBarType } from "./LoadingBarType";
|
||||
|
||||
export type LoadingPayload = { event: LoadingBarType, loader_uuid: string, fraction: number | null, message: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Log4jEvent = { timestamp_millis: number | null, logger_name: string | null, level: string | null, thread_name: string | null, message: string | null, throwable: string | null, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Log4jEvent } from "./Log4jEvent";
|
||||
|
||||
export type LogPayload = { instance_id: string, } & ({ "type": "log4j" } & Log4jEvent | { "type": "legacy", message: string, });
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ModLoader = "vanilla" | "forge" | "fabric" | "quilt" | "neoforge";
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ProcessPayloadType } from "./ProcessPayloadType";
|
||||
|
||||
export type ProcessPayload = { instance_id: string, uuid: string, event: ProcessPayloadType, message: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ProcessPayloadType = "launched" | "finished";
|
||||
@@ -0,0 +1,8 @@
|
||||
# App event bindings
|
||||
|
||||
> [!WARNING]
|
||||
> Do not edit the generated TypeScript or Postcard binding files in this directory manually.
|
||||
|
||||
The event bus types are determined by the Rust [`AppEvent`](../../../../../packages/app-lib/src/event/mod.rs) contract.
|
||||
|
||||
They are regenerated automatically when you run `pnpm app:dev` from the workspace root. Commit any generated changes with the Rust contract change.
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SharedInstanceUnavailableReason = "deleted" | "access_revoked" | "quarantined";
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type WarningPayload = { message: string, };
|
||||
@@ -0,0 +1,78 @@
|
||||
declare type u8 = number
|
||||
declare type u16 = number
|
||||
declare type u32 = number
|
||||
declare type u64 = bigint
|
||||
declare type u128 = bigint
|
||||
declare type usize = bigint
|
||||
declare type i8 = number
|
||||
declare type i16 = number
|
||||
declare type i32 = number
|
||||
declare type i64 = bigint
|
||||
declare type i128 = bigint
|
||||
declare type isize = bigint
|
||||
declare type NonZeroU8 = number
|
||||
declare type NonZeroU16 = number
|
||||
declare type NonZeroU32 = number
|
||||
declare type NonZeroU64 = bigint
|
||||
declare type NonZeroU128 = bigint
|
||||
declare type NonZeroUsize = bigint
|
||||
declare type NonZeroI8 = number
|
||||
declare type NonZeroI16 = number
|
||||
declare type NonZeroI32 = number
|
||||
declare type NonZeroI64 = bigint
|
||||
declare type NonZeroI128 = bigint
|
||||
declare type NonZeroIsize = bigint
|
||||
declare type f32 = number
|
||||
declare type f64 = number
|
||||
|
||||
declare type ArrayLengthMutationKeys = "splice" | "push" | "pop" | "shift" | "unshift"
|
||||
declare type FixedLengthArray<T, L extends number, TObj = [T, ...Array<T>]> =
|
||||
Pick<TObj, Exclude<keyof TObj, ArrayLengthMutationKeys>>
|
||||
& {
|
||||
readonly length: L
|
||||
[ I : number ] : T
|
||||
[Symbol.iterator]: () => IterableIterator<T>
|
||||
}
|
||||
|
||||
export type AppEvent = { tag: "loading", value: LoadingPayload } | { tag: "process", value: ProcessPayload } | { tag: "instance", value: InstancePayload } | { tag: "instance_bulk_update_progress", value: InstanceBulkUpdateProgressPayload } | { tag: "install_job", value: InstallJobSnapshot } | { tag: "command", value: CommandPayload } | { tag: "warning", value: WarningPayload } | { tag: "friend", value: FriendPayload } | { tag: "notification", value: string } | { tag: "log", value: LogPayload } | { tag: "ads_consent_required", value: boolean }
|
||||
export type LoadingBarType = { tag: "legacy_data_migration" } | { tag: "directory_move", value: { old: string, new: string } } | { tag: "java_download", value: { version: u32 } } | { tag: "pack_file_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_version: string } } | { tag: "pack_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_id: string | undefined, pack_version: string | undefined } } | { tag: "pack_import", value: { pack_name: string } } | { tag: "minecraft_download", value: { instance_id: string, instance_name: string } } | { tag: "instance_update", value: { instance_id: string, instance_name: string } } | { tag: "zip_extract", value: { instance_id: string, instance_name: string } } | { tag: "pack_export", value: { instance_id: string, instance_name: string } } | { tag: "config_change", value: { new_path: string } } | { tag: "copy_instance", value: { import_location: string, instance_name: string } } | { tag: "launcher_update", value: { version: string, current_version: string } }
|
||||
export type LoadingPayload = { event: LoadingBarType, loader_uuid: string, fraction: f64 | undefined, message: string }
|
||||
export type WarningPayload = { message: string }
|
||||
export type InstanceBulkUpdateProgressPayload = { instanceId: string, stage: InstanceBulkUpdateProgressStage, current: u64, total: u64 }
|
||||
export type InstanceBulkUpdateProgressStage = { tag: "resolving_versions" } | { tag: "downloading" } | { tag: "finishing" }
|
||||
export type CommandPayload = { tag: "InstallMod", value: { id: string } } | { tag: "InstallVersion", value: { id: string } } | { tag: "InstallModpack", value: { id: string } } | { tag: "InstallServer", value: { id: string } } | { tag: "LaunchInstance", value: { id: string, server: string | undefined, singleplayer_world: string | undefined } } | { tag: "InstallSharedInstanceInvite", value: { invite_id: string } } | { tag: "RunMRPack", value: { path: string } }
|
||||
export type ProcessPayload = { instance_id: string, uuid: string, event: ProcessPayloadType, message: string }
|
||||
export type ProcessPayloadType = { tag: "launched" } | { tag: "finished" }
|
||||
export type InstancePayload = { instance_id: string, event: InstancePayloadType }
|
||||
export type InstancePayloadType = { tag: "created" } | { tag: "synced" } | { tag: "servers_updated" } | { tag: "world_updated", value: { world: string } } | { tag: "server_joined", value: { host: string, port: u16, timestamp: string } } | { tag: "edited" } | { tag: "content_install_finished", value: { project_ids: string[] } } | { tag: "content_install_failed", value: { project_ids: string[], message: string } } | { tag: "removed" }
|
||||
export type FriendPayload = { tag: "friend_request", value: { from: string } } | { tag: "user_offline", value: { id: string } } | { tag: "status_update", value: { user_status: FriendStatusPayload } } | { tag: "status_sync" }
|
||||
export type FriendStatusPayload = { user_id: string, profile_name: string | undefined, last_update: string }
|
||||
export type SharedInstanceUnavailableReason = { tag: "deleted" } | { tag: "access_revoked" } | { tag: "quarantined" }
|
||||
export type LogEvent = { tag: "log4j", value: Log4jEvent } | { tag: "legacy", value: { message: string } }
|
||||
export type LogPayload = { instance_id: string, event: LogEvent }
|
||||
export type Log4jEvent = { timestamp_millis: i64 | undefined, logger_name: string | undefined, level: string | undefined, thread_name: string | undefined, message: string | undefined, throwable: string | undefined }
|
||||
export type ModLoader = { tag: "vanilla" } | { tag: "forge" } | { tag: "fabric" } | { tag: "quilt" } | { tag: "neoforge" }
|
||||
export type InstallJobSnapshot = { job_id: string, instance_id: string | undefined, kind: InstallJobKind, status: InstallJobStatus, target: InstallTarget, phase: InstallPhaseId, progress: InstallProgress | undefined, details: InstallPhaseDetails, display: InstallJobDisplay | undefined, error: InstallErrorView | undefined, rollback_error: InstallErrorView | undefined, created: string, modified: string, finished: string | undefined }
|
||||
export type InstallJobKind = { tag: "create_instance" } | { tag: "create_modpack_instance" } | { tag: "create_shared_instance" } | { tag: "import_instance" } | { tag: "duplicate_instance" } | { tag: "install_existing_instance" } | { tag: "install_pack_to_existing_instance" } | { tag: "update_shared_instance" }
|
||||
export type InstallJobStatus = { tag: "queued" } | { tag: "running" } | { tag: "succeeded" } | { tag: "failed" } | { tag: "interrupted" } | { tag: "canceled" }
|
||||
export type InstallTarget = { tag: "new_instance", value: { instance_id: string | undefined } } | { tag: "existing_instance", value: { instance_id: string } }
|
||||
export type InstallPhaseId = { tag: "preparing_instance" } | { tag: "resolving_pack" } | { tag: "downloading_pack_file" } | { tag: "reading_pack_manifest" } | { tag: "downloading_content" } | { tag: "extracting_overrides" } | { tag: "resolving_minecraft" } | { tag: "resolving_loader" } | { tag: "preparing_java" } | { tag: "downloading_minecraft" } | { tag: "running_loader_processors" } | { tag: "finalizing" } | { tag: "rolling_back" }
|
||||
export type InstallProgress = { current: u64, total: u64, secondary: InstallProgressSecondary | undefined }
|
||||
export type InstallProgressSecondary = { current: u64, total: u64 }
|
||||
export type InstallPhaseDetails = { tag: "empty" } | { tag: "instance", value: { name: string } } | { tag: "minecraft", value: { game_version: string, loader: ModLoader } } | { tag: "java", value: { major_version: u32, step: InstallJavaStep } } | { tag: "modpack", value: { project_id: string | undefined, version_id: string | undefined, title: string | undefined } } | { tag: "import", value: { launcher_type: ImportLauncherType, instance_folder: string } }
|
||||
export type InstallJavaStep = { tag: "resolving" } | { tag: "fetching_metadata" } | { tag: "downloading" } | { tag: "extracting" } | { tag: "validating" }
|
||||
export type InstallJobDisplay = { title: string, icon: string | undefined }
|
||||
export type InstallErrorView = { code: string, phase: InstallPhaseId | undefined, message: string, reason: SharedInstanceUnavailableReason | undefined, api: InstallApiErrorDetails | undefined, context: InstallErrorContext | undefined }
|
||||
export type InstallApiErrorDetails = { error: string, status: u16 | undefined, method: string | undefined, url: string | undefined, route: string | undefined }
|
||||
export type InstallErrorContext = { operation: string, source_path: string | undefined, target_path: string | undefined, file_path: string | undefined, entry_path: string | undefined, urls: string[], expected_hash: string | undefined, expected_size: u64 | undefined, project_id: string | undefined, version_id: string | undefined, minecraft_version: string | undefined, loader: string | undefined, java_version: u32 | undefined, os: string | undefined, arch: string | undefined }
|
||||
export type ImportLauncherType = { tag: "MultiMC" } | { tag: "PrismLauncher" } | { tag: "ATLauncher" } | { tag: "GDLauncher" } | { tag: "Curseforge" } | { tag: "Unknown" }
|
||||
|
||||
export type Type = "AppEvent" | "LoadingBarType" | "LoadingPayload" | "WarningPayload" | "InstanceBulkUpdateProgressPayload" | "InstanceBulkUpdateProgressStage" | "CommandPayload" | "ProcessPayload" | "ProcessPayloadType" | "InstancePayload" | "InstancePayloadType" | "FriendPayload" | "FriendStatusPayload" | "LogEvent" | "LogPayload" | "Log4jEvent" | "InstallJobSnapshot" | "InstallJobKind" | "InstallJobStatus" | "InstallTarget" | "InstallPhaseId" | "InstallProgress" | "InstallProgressSecondary" | "InstallPhaseDetails" | "InstallJavaStep" | "InstallJobDisplay" | "InstallErrorView" | "InstallApiErrorDetails" | "InstallErrorContext" | "ImportLauncherType" | "ModLoader" | "SharedInstanceUnavailableReason"
|
||||
declare type ValueType<T extends Type> = T extends "AppEvent" ? AppEvent : T extends "LoadingBarType" ? LoadingBarType : T extends "LoadingPayload" ? LoadingPayload : T extends "WarningPayload" ? WarningPayload : T extends "InstanceBulkUpdateProgressPayload" ? InstanceBulkUpdateProgressPayload : T extends "InstanceBulkUpdateProgressStage" ? InstanceBulkUpdateProgressStage : T extends "CommandPayload" ? CommandPayload : T extends "ProcessPayload" ? ProcessPayload : T extends "ProcessPayloadType" ? ProcessPayloadType : T extends "InstancePayload" ? InstancePayload : T extends "InstancePayloadType" ? InstancePayloadType : T extends "FriendPayload" ? FriendPayload : T extends "FriendStatusPayload" ? FriendStatusPayload : T extends "LogEvent" ? LogEvent : T extends "LogPayload" ? LogPayload : T extends "Log4jEvent" ? Log4jEvent : T extends "InstallJobSnapshot" ? InstallJobSnapshot : T extends "InstallJobKind" ? InstallJobKind : T extends "InstallJobStatus" ? InstallJobStatus : T extends "InstallTarget" ? InstallTarget : T extends "InstallPhaseId" ? InstallPhaseId : T extends "InstallProgress" ? InstallProgress : T extends "InstallProgressSecondary" ? InstallProgressSecondary : T extends "InstallPhaseDetails" ? InstallPhaseDetails : T extends "InstallJavaStep" ? InstallJavaStep : T extends "InstallJobDisplay" ? InstallJobDisplay : T extends "InstallErrorView" ? InstallErrorView : T extends "InstallApiErrorDetails" ? InstallApiErrorDetails : T extends "InstallErrorContext" ? InstallErrorContext : T extends "ImportLauncherType" ? ImportLauncherType : T extends "ModLoader" ? ModLoader : T extends "SharedInstanceUnavailableReason" ? SharedInstanceUnavailableReason : void
|
||||
|
||||
export interface Result<T extends Type> {
|
||||
value: ValueType<T>;
|
||||
bytes: Uint8Array;
|
||||
}
|
||||
|
||||
export function deserialize<T extends Type>(type: T, bytes: Uint8Array): Result<T>
|
||||
@@ -0,0 +1,968 @@
|
||||
const BITS_PER_BYTE = 8, BITS_PER_VARINT_BYTE = 7, U8_BYTES = 1, U16_BYTES = 2, U32_BYTES = 4, U64_BYTES = 8, U128_BYTES = 16
|
||||
|
||||
const de_zig_zag_signed = (n) => (n >> 1n) ^ (-(n & 0b1n))
|
||||
const zig_zag = (n_bytes, n) => (n << 1n) ^ (n >> BigInt(n_bytes * BITS_PER_BYTE - 1))
|
||||
const varint_max = (n_bytes) => Math.floor((n_bytes * BITS_PER_BYTE + (BITS_PER_BYTE - 1)) / BITS_PER_VARINT_BYTE)
|
||||
const max_of_last_byte = (n_bytes) => (1 << (n_bytes * BITS_PER_BYTE) % 7) - 1
|
||||
const to_number_if_safe = (n) => Number.MAX_SAFE_INTEGER < ((n < 0n) ? -n : n) ? n : Number(n)
|
||||
const varint = (n_bytes, n) => { let value = BigInt(n), out = []; for (let i = 0; i < varint_max(n_bytes); i++) { out.push(Number(value & 0xFFn)); if (value < 128n) { return out } out[i] |= 0x80; value >>= 7n } }
|
||||
|
||||
class Deserializer {
|
||||
constructor(bytes_in) { this.bytes = Array.from(bytes_in); }
|
||||
pop_next = () => { const next = this.bytes.shift(); if (next === undefined) { throw "input buffer too small" } return next }
|
||||
pop_n = (n) => { const bytes = Array(); for (let i = 0; i < n; i++) { bytes.push(this.bytes.shift()) } return bytes }
|
||||
get_int8 = (signed) => signed ? new Int8Array([this.pop_next()])[0] : this.pop_next();
|
||||
try_take = (n_bytes) => { let out = 0n, v_max = varint_max(n_bytes); for (let i = 0; i < v_max; i++) { const val = this.pop_next(), carry = BigInt(val & 0x7F); out |= carry << BigInt(7 * i); if ((val & 0x80) === 0) { if (i === v_max - 1 && val > max_of_last_byte(n_bytes)) { throw "Bad Variant" } else return out } } throw "Bad Variant"; }
|
||||
deserialize_bool = () => { const byte = this.pop_next(); return byte === undefined ? undefined : byte > 0 ? true : false }
|
||||
deserialize_number = (n_bytes, signed) => { if (n_bytes === U8_BYTES) { return this.get_int8(signed) } else if (n_bytes === U16_BYTES || n_bytes === U32_BYTES || n_bytes === U64_BYTES || n_bytes === U128_BYTES) { const val = this.try_take(n_bytes); return to_number_if_safe(signed ? de_zig_zag_signed(val) : val) } else { throw "byte count not supported" } }
|
||||
deserialize_number_float = (n_bytes) => { const b_buffer = new ArrayBuffer(n_bytes), b_view = new DataView(b_buffer); this.pop_n(n_bytes).forEach((b, i) => b_view.setUint8(i, b)); if (n_bytes === U32_BYTES) { return b_view.getFloat32(0, true) } else if (n_bytes === U64_BYTES) { return b_view.getFloat64(0, true) } else { throw "byte count not supported" } }
|
||||
deserialize_string = () => new TextDecoder().decode(new Uint8Array(this.pop_n(Number(this.try_take(U32_BYTES)))))
|
||||
deserialize_array = (des, len) => Array.from({length: len === undefined ? Number(this.try_take(U32_BYTES)) : len}, (v, i) => des(this))
|
||||
deserialize_string_key_map = (des) => { return [...Array(Number(this.try_take(U32_BYTES)))].reduce((prev) => { prev[this.deserialize_string()] = des(this); return prev }, {}) }
|
||||
deserialize_map = (des) => { return [...Array(Number(this.try_take(U32_BYTES)))].reduce((prev) => { const d = des(this); prev.set(d[0], d[1]); return prev }, new Map()) }
|
||||
release_bytes = () => { return new Uint8Array(this.bytes); }
|
||||
}
|
||||
|
||||
function deserialize_APP_EVENT(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "loading",
|
||||
value: deserialize_LOADING_PAYLOAD(d)
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "process",
|
||||
value: deserialize_PROCESS_PAYLOAD(d)
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "instance",
|
||||
value: deserialize_INSTANCE_PAYLOAD(d)
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "instance_bulk_update_progress",
|
||||
value: deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d)
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "install_job",
|
||||
value: deserialize_INSTALL_JOB_SNAPSHOT(d)
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "command",
|
||||
value: deserialize_COMMAND_PAYLOAD(d)
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
tag: "warning",
|
||||
value: deserialize_WARNING_PAYLOAD(d)
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
tag: "friend",
|
||||
value: deserialize_FRIEND_PAYLOAD(d)
|
||||
};
|
||||
case 8:
|
||||
return {
|
||||
tag: "notification",
|
||||
value: d.deserialize_string()
|
||||
};
|
||||
case 9:
|
||||
return {
|
||||
tag: "log",
|
||||
value: deserialize_LOG_PAYLOAD(d)
|
||||
};
|
||||
case 10:
|
||||
return {
|
||||
tag: "ads_consent_required",
|
||||
value: d.deserialize_bool()
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_LOADING_BAR_TYPE(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "legacy_data_migration"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "directory_move",
|
||||
value: {
|
||||
old: d.deserialize_string(),
|
||||
new: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "java_download",
|
||||
value: {
|
||||
version: d.deserialize_number(U32_BYTES, false)
|
||||
}
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "pack_file_download",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
pack_name: d.deserialize_string(),
|
||||
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
pack_version: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "pack_download",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
pack_name: d.deserialize_string(),
|
||||
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
pack_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
pack_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "pack_import",
|
||||
value: {
|
||||
pack_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
tag: "minecraft_download",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
tag: "instance_update",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 8:
|
||||
return {
|
||||
tag: "zip_extract",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 9:
|
||||
return {
|
||||
tag: "pack_export",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 10:
|
||||
return {
|
||||
tag: "config_change",
|
||||
value: {
|
||||
new_path: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 11:
|
||||
return {
|
||||
tag: "copy_instance",
|
||||
value: {
|
||||
import_location: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 12:
|
||||
return {
|
||||
tag: "launcher_update",
|
||||
value: {
|
||||
version: d.deserialize_string(),
|
||||
current_version: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_LOADING_PAYLOAD(d) {
|
||||
return {
|
||||
event: deserialize_LOADING_BAR_TYPE(d),
|
||||
loader_uuid: d.deserialize_string(),
|
||||
fraction: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number_float(U64_BYTES),
|
||||
message: d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_WARNING_PAYLOAD(d) {
|
||||
return {
|
||||
message: d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d) {
|
||||
return {
|
||||
instanceId: d.deserialize_string(),
|
||||
stage: deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d),
|
||||
current: d.deserialize_number(U64_BYTES, false),
|
||||
total: d.deserialize_number(U64_BYTES, false)
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "resolving_versions"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "downloading"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "finishing"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_COMMAND_PAYLOAD(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "InstallMod",
|
||||
value: {
|
||||
id: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "InstallVersion",
|
||||
value: {
|
||||
id: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "InstallModpack",
|
||||
value: {
|
||||
id: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "InstallServer",
|
||||
value: {
|
||||
id: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "LaunchInstance",
|
||||
value: {
|
||||
id: d.deserialize_string(),
|
||||
server: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
singleplayer_world: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "InstallSharedInstanceInvite",
|
||||
value: {
|
||||
invite_id: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
tag: "RunMRPack",
|
||||
value: {
|
||||
path: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_PROCESS_PAYLOAD(d) {
|
||||
return {
|
||||
instance_id: d.deserialize_string(),
|
||||
uuid: d.deserialize_string(),
|
||||
event: deserialize_PROCESS_PAYLOAD_TYPE(d),
|
||||
message: d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_PROCESS_PAYLOAD_TYPE(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "launched"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "finished"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_INSTANCE_PAYLOAD(d) {
|
||||
return {
|
||||
instance_id: d.deserialize_string(),
|
||||
event: deserialize_INSTANCE_PAYLOAD_TYPE(d)
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTANCE_PAYLOAD_TYPE(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "created"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "synced"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "servers_updated"
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "world_updated",
|
||||
value: {
|
||||
world: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "server_joined",
|
||||
value: {
|
||||
host: d.deserialize_string(),
|
||||
port: d.deserialize_number(U16_BYTES, false),
|
||||
timestamp: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "edited"
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
tag: "content_install_finished",
|
||||
value: {
|
||||
project_ids: d.deserialize_array(() => d.deserialize_string())
|
||||
}
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
tag: "content_install_failed",
|
||||
value: {
|
||||
project_ids: d.deserialize_array(() => d.deserialize_string()),
|
||||
message: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 8:
|
||||
return {
|
||||
tag: "removed"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_FRIEND_PAYLOAD(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "friend_request",
|
||||
value: {
|
||||
from: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "user_offline",
|
||||
value: {
|
||||
id: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "status_update",
|
||||
value: {
|
||||
user_status: deserialize_FRIEND_STATUS_PAYLOAD(d)
|
||||
}
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "status_sync"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_FRIEND_STATUS_PAYLOAD(d) {
|
||||
return {
|
||||
user_id: d.deserialize_string(),
|
||||
profile_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
last_update: d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_LOG_EVENT(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "log4j",
|
||||
value: deserialize_LOG4J_EVENT(d)
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "legacy",
|
||||
value: {
|
||||
message: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_LOG_PAYLOAD(d) {
|
||||
return {
|
||||
instance_id: d.deserialize_string(),
|
||||
event: deserialize_LOG_EVENT(d)
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_LOG4J_EVENT(d) {
|
||||
return {
|
||||
timestamp_millis: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U64_BYTES, true),
|
||||
logger_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
level: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
thread_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
message: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
throwable: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_JOB_SNAPSHOT(d) {
|
||||
return {
|
||||
job_id: d.deserialize_string(),
|
||||
instance_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
kind: deserialize_INSTALL_JOB_KIND(d),
|
||||
status: deserialize_INSTALL_JOB_STATUS(d),
|
||||
target: deserialize_INSTALL_TARGET(d),
|
||||
phase: deserialize_INSTALL_PHASE_ID(d),
|
||||
progress: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PROGRESS(d),
|
||||
details: deserialize_INSTALL_PHASE_DETAILS(d),
|
||||
display: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_JOB_DISPLAY(d),
|
||||
error: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_VIEW(d),
|
||||
rollback_error: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_VIEW(d),
|
||||
created: d.deserialize_string(),
|
||||
modified: d.deserialize_string(),
|
||||
finished: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_JOB_KIND(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "create_instance"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "create_modpack_instance"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "create_shared_instance"
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "import_instance"
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "duplicate_instance"
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "install_existing_instance"
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
tag: "install_pack_to_existing_instance"
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
tag: "update_shared_instance"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_JOB_STATUS(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "queued"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "running"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "succeeded"
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "failed"
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "interrupted"
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "canceled"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_TARGET(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "new_instance",
|
||||
value: {
|
||||
instance_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "existing_instance",
|
||||
value: {
|
||||
instance_id: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_PHASE_ID(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "preparing_instance"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "resolving_pack"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "downloading_pack_file"
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "reading_pack_manifest"
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "downloading_content"
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "extracting_overrides"
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
tag: "resolving_minecraft"
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
tag: "resolving_loader"
|
||||
};
|
||||
case 8:
|
||||
return {
|
||||
tag: "preparing_java"
|
||||
};
|
||||
case 9:
|
||||
return {
|
||||
tag: "downloading_minecraft"
|
||||
};
|
||||
case 10:
|
||||
return {
|
||||
tag: "running_loader_processors"
|
||||
};
|
||||
case 11:
|
||||
return {
|
||||
tag: "finalizing"
|
||||
};
|
||||
case 12:
|
||||
return {
|
||||
tag: "rolling_back"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_PROGRESS(d) {
|
||||
return {
|
||||
current: d.deserialize_number(U64_BYTES, false),
|
||||
total: d.deserialize_number(U64_BYTES, false),
|
||||
secondary: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PROGRESS_SECONDARY(d)
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_PROGRESS_SECONDARY(d) {
|
||||
return {
|
||||
current: d.deserialize_number(U64_BYTES, false),
|
||||
total: d.deserialize_number(U64_BYTES, false)
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_PHASE_DETAILS(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "empty"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "instance",
|
||||
value: {
|
||||
name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "minecraft",
|
||||
value: {
|
||||
game_version: d.deserialize_string(),
|
||||
loader: deserialize_MOD_LOADER(d)
|
||||
}
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "java",
|
||||
value: {
|
||||
major_version: d.deserialize_number(U32_BYTES, false),
|
||||
step: deserialize_INSTALL_JAVA_STEP(d)
|
||||
}
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "modpack",
|
||||
value: {
|
||||
project_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
version_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
title: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "import",
|
||||
value: {
|
||||
launcher_type: deserialize_IMPORT_LAUNCHER_TYPE(d),
|
||||
instance_folder: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_JAVA_STEP(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "resolving"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "fetching_metadata"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "downloading"
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "extracting"
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "validating"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_JOB_DISPLAY(d) {
|
||||
return {
|
||||
title: d.deserialize_string(),
|
||||
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_ERROR_VIEW(d) {
|
||||
return {
|
||||
code: d.deserialize_string(),
|
||||
phase: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PHASE_ID(d),
|
||||
message: d.deserialize_string(),
|
||||
reason: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d),
|
||||
api: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_API_ERROR_DETAILS(d),
|
||||
context: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_CONTEXT(d)
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_API_ERROR_DETAILS(d) {
|
||||
return {
|
||||
error: d.deserialize_string(),
|
||||
status: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U16_BYTES, false),
|
||||
method: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
url: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
route: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_INSTALL_ERROR_CONTEXT(d) {
|
||||
return {
|
||||
operation: d.deserialize_string(),
|
||||
source_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
target_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
file_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
entry_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
urls: d.deserialize_array(() => d.deserialize_string()),
|
||||
expected_hash: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
expected_size: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U64_BYTES, false),
|
||||
project_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
version_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
minecraft_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
loader: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
java_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U32_BYTES, false),
|
||||
os: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
|
||||
arch: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
|
||||
};
|
||||
}
|
||||
|
||||
function deserialize_IMPORT_LAUNCHER_TYPE(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "MultiMC"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "PrismLauncher"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "ATLauncher"
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "GDLauncher"
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "Curseforge"
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
tag: "Unknown"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_MOD_LOADER(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "vanilla"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "forge"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "fabric"
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
tag: "quilt"
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
tag: "neoforge"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d) {
|
||||
switch (d.deserialize_number(U32_BYTES, false)) {
|
||||
case 0:
|
||||
return {
|
||||
tag: "deleted"
|
||||
};
|
||||
case 1:
|
||||
return {
|
||||
tag: "access_revoked"
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
tag: "quarantined"
|
||||
};
|
||||
default:
|
||||
throw "variant not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a value from an array of bytes.
|
||||
* @param {string} type - The type of the value to deserialize.
|
||||
* @param {Uint8Array} bytes - The byte array to deserialize from.
|
||||
* @return {Object} The deserialized value and remaining bytes.
|
||||
*/
|
||||
function deserialize(type, bytes) {
|
||||
if (!(typeof type === "string")) {
|
||||
throw "type must be a string";
|
||||
}
|
||||
const d = new Deserializer(bytes);
|
||||
var return_value = undefined;
|
||||
switch (type) {
|
||||
case "AppEvent":
|
||||
return_value = deserialize_APP_EVENT(d);
|
||||
break;
|
||||
case "LoadingBarType":
|
||||
return_value = deserialize_LOADING_BAR_TYPE(d);
|
||||
break;
|
||||
case "LoadingPayload":
|
||||
return_value = deserialize_LOADING_PAYLOAD(d);
|
||||
break;
|
||||
case "WarningPayload":
|
||||
return_value = deserialize_WARNING_PAYLOAD(d);
|
||||
break;
|
||||
case "InstanceBulkUpdateProgressPayload":
|
||||
return_value = deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d);
|
||||
break;
|
||||
case "InstanceBulkUpdateProgressStage":
|
||||
return_value = deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d);
|
||||
break;
|
||||
case "CommandPayload":
|
||||
return_value = deserialize_COMMAND_PAYLOAD(d);
|
||||
break;
|
||||
case "ProcessPayload":
|
||||
return_value = deserialize_PROCESS_PAYLOAD(d);
|
||||
break;
|
||||
case "ProcessPayloadType":
|
||||
return_value = deserialize_PROCESS_PAYLOAD_TYPE(d);
|
||||
break;
|
||||
case "InstancePayload":
|
||||
return_value = deserialize_INSTANCE_PAYLOAD(d);
|
||||
break;
|
||||
case "InstancePayloadType":
|
||||
return_value = deserialize_INSTANCE_PAYLOAD_TYPE(d);
|
||||
break;
|
||||
case "FriendPayload":
|
||||
return_value = deserialize_FRIEND_PAYLOAD(d);
|
||||
break;
|
||||
case "FriendStatusPayload":
|
||||
return_value = deserialize_FRIEND_STATUS_PAYLOAD(d);
|
||||
break;
|
||||
case "LogEvent":
|
||||
return_value = deserialize_LOG_EVENT(d);
|
||||
break;
|
||||
case "LogPayload":
|
||||
return_value = deserialize_LOG_PAYLOAD(d);
|
||||
break;
|
||||
case "Log4jEvent":
|
||||
return_value = deserialize_LOG4J_EVENT(d);
|
||||
break;
|
||||
case "InstallJobSnapshot":
|
||||
return_value = deserialize_INSTALL_JOB_SNAPSHOT(d);
|
||||
break;
|
||||
case "InstallJobKind":
|
||||
return_value = deserialize_INSTALL_JOB_KIND(d);
|
||||
break;
|
||||
case "InstallJobStatus":
|
||||
return_value = deserialize_INSTALL_JOB_STATUS(d);
|
||||
break;
|
||||
case "InstallTarget":
|
||||
return_value = deserialize_INSTALL_TARGET(d);
|
||||
break;
|
||||
case "InstallPhaseId":
|
||||
return_value = deserialize_INSTALL_PHASE_ID(d);
|
||||
break;
|
||||
case "InstallProgress":
|
||||
return_value = deserialize_INSTALL_PROGRESS(d);
|
||||
break;
|
||||
case "InstallProgressSecondary":
|
||||
return_value = deserialize_INSTALL_PROGRESS_SECONDARY(d);
|
||||
break;
|
||||
case "InstallPhaseDetails":
|
||||
return_value = deserialize_INSTALL_PHASE_DETAILS(d);
|
||||
break;
|
||||
case "InstallJavaStep":
|
||||
return_value = deserialize_INSTALL_JAVA_STEP(d);
|
||||
break;
|
||||
case "InstallJobDisplay":
|
||||
return_value = deserialize_INSTALL_JOB_DISPLAY(d);
|
||||
break;
|
||||
case "InstallErrorView":
|
||||
return_value = deserialize_INSTALL_ERROR_VIEW(d);
|
||||
break;
|
||||
case "InstallApiErrorDetails":
|
||||
return_value = deserialize_INSTALL_API_ERROR_DETAILS(d);
|
||||
break;
|
||||
case "InstallErrorContext":
|
||||
return_value = deserialize_INSTALL_ERROR_CONTEXT(d);
|
||||
break;
|
||||
case "ImportLauncherType":
|
||||
return_value = deserialize_IMPORT_LAUNCHER_TYPE(d);
|
||||
break;
|
||||
case "ModLoader":
|
||||
return_value = deserialize_MOD_LOADER(d);
|
||||
break;
|
||||
case "SharedInstanceUnavailableReason":
|
||||
return_value = deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d);
|
||||
break;
|
||||
default:
|
||||
throw "type not implemented";
|
||||
}
|
||||
return { value: return_value, bytes: d.release_bytes() };
|
||||
}
|
||||
|
||||
export {
|
||||
deserialize
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "postcard",
|
||||
"description": "Auto generated bindings for postcard format serializing and deserializing javascript to and from bytes.",
|
||||
"version": "0.0.0",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"type": "module"
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
export async function init_ads_window(overrideShown = false) {
|
||||
return await invoke('plugin:ads|init_ads_window', {
|
||||
@@ -47,10 +46,6 @@ export async function open_ads_consent_preferences() {
|
||||
return await invoke('plugin:ads|open_ads_consent_preferences')
|
||||
}
|
||||
|
||||
export async function ads_consent_listener(callback) {
|
||||
return await listen('ads-consent-required', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function record_ads_click() {
|
||||
return await invoke('plugin:ads|record_ads_click')
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
Event listeners for interacting with the Rust api
|
||||
These are all async functions that return a promise that resolves to the payload object (whatever Rust is trying to deliver)
|
||||
*/
|
||||
|
||||
/*
|
||||
callback is a function that takes a single argument, which is the payload object (whatever Rust is trying to deliver)
|
||||
|
||||
You can call these to await any kind of emitted signal from Rust, and then do something with the payload object
|
||||
An example place to put this is at the start of main.js before the state is initialized- that way
|
||||
you can listen for any emitted signal from Rust and do something with it as the state is being initialized
|
||||
|
||||
Example:
|
||||
import { loading_listener } from '@/helpers/events'
|
||||
await loading_listener((event) => {
|
||||
// event.event is the event name (useful if you want to use a single callback fn for multiple event types)
|
||||
// event.payload is the payload object
|
||||
console.log(event)
|
||||
})
|
||||
|
||||
Putting that in a script will print any emitted signal from rust
|
||||
*/
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
/// Payload for the 'loading' event
|
||||
/*
|
||||
LoadingPayload {
|
||||
event: {
|
||||
type: string, one of "StateInit", "PackDownload", etc
|
||||
(Optional fields depending on event type)
|
||||
pack_name: name of the pack
|
||||
pack_id, optional, the id of the modpack
|
||||
pack_version, optional, the version of the modpack
|
||||
instance_name: name of the instance
|
||||
instance_id: unique identification of the instance
|
||||
|
||||
}
|
||||
loader_uuid: unique identification of the loading bar
|
||||
fraction: number, (as a fraction of 1, how much we've loaded so far). If null, by convention, loading is finished
|
||||
message: message to display to the user
|
||||
}
|
||||
*/
|
||||
export async function loading_listener(callback) {
|
||||
return await listen('loading', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'process' event
|
||||
/*
|
||||
ProcessPayload {
|
||||
uuid: unique identification of the process in the state (currently identified by PID, but that will change)
|
||||
pid: process ID
|
||||
event: event type ("Launched", "Finished")
|
||||
message: message to display to the user
|
||||
}
|
||||
*/
|
||||
export async function process_listener(callback) {
|
||||
return await listen('process', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'instance' event
|
||||
/*
|
||||
InstancePayload {
|
||||
instance_id: unique identification of the instance
|
||||
event: event type ("Created", "Added", "Edited", "Removed")
|
||||
}
|
||||
*/
|
||||
export async function instance_listener(callback) {
|
||||
return await listen('instance', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'instance_bulk_update_progress' event
|
||||
/*
|
||||
InstanceBulkUpdateProgress {
|
||||
instanceId: string
|
||||
stage: "resolving_versions" | "downloading" | "finishing"
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
*/
|
||||
export async function instance_bulk_update_progress_listener(callback) {
|
||||
return await listen('instance_bulk_update_progress', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function install_job_listener(callback) {
|
||||
return await listen('install_job', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'command' event
|
||||
/*
|
||||
CommandPayload {
|
||||
event: event type ("InstallMod", "InstallModpack", "InstallVersion"),
|
||||
id: string id of the mod/modpack/version to install
|
||||
}
|
||||
*/
|
||||
export async function command_listener(callback) {
|
||||
return await listen('command', (event) => {
|
||||
callback(event.payload)
|
||||
})
|
||||
}
|
||||
|
||||
/// Payload for the 'warning' event
|
||||
/*
|
||||
WarningPayload {
|
||||
message: message to display to the user
|
||||
}
|
||||
*/
|
||||
export async function warning_listener(callback) {
|
||||
return await listen('warning', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function friend_listener(callback) {
|
||||
return await listen('friend', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
export async function notification_listener(callback) {
|
||||
return await listen('notification', (event) => callback(event.payload))
|
||||
}
|
||||
|
||||
/// Payload for the 'log' event
|
||||
/*
|
||||
LogPayload {
|
||||
instance_id: string,
|
||||
type: "log4j" | "legacy",
|
||||
// log4j fields (when type === "log4j"):
|
||||
timestamp_millis?: number,
|
||||
logger_name?: string,
|
||||
level?: string,
|
||||
thread_name?: string,
|
||||
message?: string,
|
||||
throwable?: string,
|
||||
// legacy fields (when type === "legacy"):
|
||||
message?: string,
|
||||
}
|
||||
*/
|
||||
export async function log_listener(callback) {
|
||||
return await listen('log', (event) => callback(event.payload))
|
||||
}
|
||||
@@ -1,8 +1,28 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import { install_job_listener } from './events'
|
||||
import type { InstallErrorView } from '@/generated/app-events/InstallErrorView'
|
||||
import type { InstallJavaStep } from '@/generated/app-events/InstallJavaStep'
|
||||
import type { InstallJobSnapshot } from '@/generated/app-events/InstallJobSnapshot'
|
||||
import type { InstallJobStatus } from '@/generated/app-events/InstallJobStatus'
|
||||
import type { InstallPhaseId } from '@/generated/app-events/InstallPhaseId'
|
||||
import type { InstallProgress } from '@/generated/app-events/InstallProgress'
|
||||
import type { InstallProgressSecondary } from '@/generated/app-events/InstallProgressSecondary'
|
||||
import type { SharedInstanceUnavailableReason } from '@/generated/app-events/SharedInstanceUnavailableReason'
|
||||
import type { AppEvents } from '@/providers/app-events'
|
||||
|
||||
import type { InstanceLink, InstanceLoader } from './types'
|
||||
|
||||
export type {
|
||||
InstallErrorView,
|
||||
InstallJavaStep,
|
||||
InstallJobSnapshot,
|
||||
InstallJobStatus,
|
||||
InstallPhaseId,
|
||||
InstallProgress,
|
||||
InstallProgressSecondary,
|
||||
SharedInstanceUnavailableReason,
|
||||
}
|
||||
|
||||
export interface PackLocationVersionId {
|
||||
type: 'fromVersionId'
|
||||
project_id: string
|
||||
@@ -104,8 +124,6 @@ export interface SharedInstanceUpdateDiff {
|
||||
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
|
||||
export const SHARED_INSTANCES_API_ERROR_CODE = 'shared_instances_api_error'
|
||||
|
||||
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked' | 'quarantined'
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
@@ -136,116 +154,6 @@ export function getErrorMessage(error: unknown): string {
|
||||
return 'Unknown error'
|
||||
}
|
||||
|
||||
export type InstallJobStatus =
|
||||
| 'queued'
|
||||
| 'running'
|
||||
| 'succeeded'
|
||||
| 'failed'
|
||||
| 'interrupted'
|
||||
| 'canceled'
|
||||
|
||||
export type InstallPhaseId =
|
||||
| 'preparing_instance'
|
||||
| 'resolving_pack'
|
||||
| 'downloading_pack_file'
|
||||
| 'reading_pack_manifest'
|
||||
| 'downloading_content'
|
||||
| 'extracting_overrides'
|
||||
| 'resolving_minecraft'
|
||||
| 'resolving_loader'
|
||||
| 'preparing_java'
|
||||
| 'downloading_minecraft'
|
||||
| 'running_loader_processors'
|
||||
| 'finalizing'
|
||||
| 'rolling_back'
|
||||
|
||||
export interface InstallProgress {
|
||||
current: number
|
||||
total: number
|
||||
secondary?: InstallProgressSecondary | null
|
||||
}
|
||||
|
||||
export interface InstallProgressSecondary {
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type InstallJavaStep =
|
||||
| 'resolving'
|
||||
| 'fetching_metadata'
|
||||
| 'downloading'
|
||||
| 'extracting'
|
||||
| 'validating'
|
||||
|
||||
export interface InstallErrorView {
|
||||
code: string
|
||||
phase?: InstallPhaseId | null
|
||||
message: string
|
||||
reason?: SharedInstanceUnavailableReason | null
|
||||
api?: {
|
||||
error: string
|
||||
status?: number | null
|
||||
method?: string | null
|
||||
url?: string | null
|
||||
route?: string | null
|
||||
} | null
|
||||
context?: {
|
||||
operation: string
|
||||
source_path?: string | null
|
||||
target_path?: string | null
|
||||
file_path?: string | null
|
||||
entry_path?: string | null
|
||||
urls?: string[]
|
||||
expected_hash?: string | null
|
||||
expected_size?: number | null
|
||||
project_id?: string | null
|
||||
version_id?: string | null
|
||||
minecraft_version?: string | null
|
||||
loader?: string | null
|
||||
java_version?: number | null
|
||||
os?: string | null
|
||||
arch?: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface InstallJobSnapshot {
|
||||
job_id: string
|
||||
instance_id?: string | null
|
||||
kind:
|
||||
| 'create_instance'
|
||||
| 'create_modpack_instance'
|
||||
| 'create_shared_instance'
|
||||
| 'update_shared_instance'
|
||||
| 'import_instance'
|
||||
| 'duplicate_instance'
|
||||
| 'install_existing_instance'
|
||||
| 'install_pack_to_existing_instance'
|
||||
status: InstallJobStatus
|
||||
target:
|
||||
| { type: 'new_instance'; instance_id?: string | null }
|
||||
| { type: 'existing_instance'; instance_id: string }
|
||||
phase: InstallPhaseId
|
||||
progress?: InstallProgress | null
|
||||
details:
|
||||
| { type: 'empty' }
|
||||
| { type: 'instance'; name: string }
|
||||
| { type: 'minecraft'; game_version: string; loader: InstanceLoader }
|
||||
| { type: 'java'; major_version: number; step: InstallJavaStep }
|
||||
| {
|
||||
type: 'modpack'
|
||||
project_id?: string | null
|
||||
version_id?: string | null
|
||||
title?: string | null
|
||||
}
|
||||
| { type: 'import'; launcher_type: string; instance_folder: string }
|
||||
display?: { title: string; icon?: string | null } | null
|
||||
error?: InstallErrorView | null
|
||||
rollback_error?: InstallErrorView | null
|
||||
created: string
|
||||
modified: string
|
||||
finished?: string | null
|
||||
}
|
||||
|
||||
export async function install_get_modpack_preview(location: CreatePackLocation) {
|
||||
return await invoke<InstallModpackPreview>('plugin:install|install_get_modpack_preview', {
|
||||
location,
|
||||
@@ -399,7 +307,7 @@ function settleInstallJob(job: InstallJobSnapshot) {
|
||||
throw new Error(`Install job ${job.job_id} ${job.status}`)
|
||||
}
|
||||
|
||||
export async function wait_for_install_job(jobId: string) {
|
||||
export async function wait_for_install_job(events: AppEvents, jobId: string) {
|
||||
const current = await install_job_get(jobId)
|
||||
if (isInstallJobFinished(current.status)) return settleInstallJob(current)
|
||||
|
||||
@@ -434,16 +342,7 @@ export async function wait_for_install_job(jobId: string) {
|
||||
reject(err)
|
||||
}
|
||||
|
||||
install_job_listener(resolveJob)
|
||||
.then((listener) => {
|
||||
if (finished) {
|
||||
listener()
|
||||
return
|
||||
}
|
||||
|
||||
unlisten = listener
|
||||
install_job_get(jobId).then(resolveJob).catch(rejectWait)
|
||||
})
|
||||
.catch(rejectWait)
|
||||
unlisten = events.on('install_job', resolveJob)
|
||||
install_job_get(jobId).then(resolveJob).catch(rejectWait)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export type InstanceContentData = {
|
||||
|
||||
export type InstanceContentModpackData = {
|
||||
project: ManagedContentProject
|
||||
version: ManagedContentVersion
|
||||
version: ManagedContentVersion | null
|
||||
updateVersionId: string | null
|
||||
}
|
||||
|
||||
@@ -53,10 +53,12 @@ function normalizeLinkedModpackInfo(
|
||||
slug: modpackInfo.project.slug ?? modpackInfo.project.id,
|
||||
icon_url: modpackInfo.project.icon_url ?? undefined,
|
||||
},
|
||||
version: {
|
||||
...modpackInfo.version,
|
||||
date_published: modpackInfo.version.date_published.toString(),
|
||||
},
|
||||
version: modpackInfo.version
|
||||
? {
|
||||
...modpackInfo.version,
|
||||
date_published: modpackInfo.version.date_published.toString(),
|
||||
}
|
||||
: null,
|
||||
updateVersionId: modpackInfo.update_version_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function refresh_content_updates(instanceId: string): Promise<void>
|
||||
// Linked modpack info returned from backend
|
||||
export interface LinkedModpackInfo {
|
||||
project: Labrinth.Projects.v2.Project
|
||||
version: Labrinth.Versions.v2.Version
|
||||
version: Labrinth.Versions.v2.Version | null
|
||||
owner: ContentOwner | null
|
||||
has_update: boolean
|
||||
update_version_id: string | null
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
|
||||
* and deserialized into a usable JS object.
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { type Channel, invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import type { AppEvent } from '@/generated/app-events/AppEvent'
|
||||
|
||||
export interface LoadingBarType {
|
||||
type?: string
|
||||
@@ -24,24 +26,12 @@ export interface LoadingBar {
|
||||
bar_type?: LoadingBarType
|
||||
}
|
||||
|
||||
export type OpeningCommandEvent =
|
||||
| 'RunMRPack'
|
||||
| 'InstallServer'
|
||||
| 'InstallVersion'
|
||||
| 'InstallMod'
|
||||
| 'InstallModpack'
|
||||
| string
|
||||
|
||||
export interface OpeningCommand {
|
||||
event: OpeningCommandEvent
|
||||
id?: string
|
||||
path?: string
|
||||
}
|
||||
export type OpeningCommand = Extract<AppEvent, { type: 'command' }>['payload']
|
||||
|
||||
// Initialize the theseus API state
|
||||
// This should be called during the initializion/opening of the launcher
|
||||
export async function initialize_state() {
|
||||
return await invoke<void>('initialize_state')
|
||||
export async function initialize_state(events: Channel<ArrayBuffer>) {
|
||||
return await invoke<void>('initialize_state', { events })
|
||||
}
|
||||
|
||||
// Gets active progress bars
|
||||
|
||||
@@ -3,6 +3,7 @@ import { autoToHTML } from '@sfirew/minecraft-motd-parser'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import type { InstancePayload } from '@/generated/app-events/InstancePayload'
|
||||
import { get_full_path } from '@/helpers/instance'
|
||||
import { openPath } from '@/helpers/utils'
|
||||
|
||||
@@ -532,18 +533,4 @@ export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVer
|
||||
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
|
||||
}
|
||||
|
||||
export type InstanceEvent = { instance_id: string } & (
|
||||
| {
|
||||
event: 'servers_updated'
|
||||
}
|
||||
| {
|
||||
event: 'world_updated'
|
||||
world: string
|
||||
}
|
||||
| {
|
||||
event: 'server_joined'
|
||||
host: string
|
||||
port: number
|
||||
timestamp: string
|
||||
}
|
||||
)
|
||||
export type InstanceEvent = InstancePayload
|
||||
|
||||
@@ -287,6 +287,12 @@
|
||||
"app.export-modal.export-button": {
|
||||
"message": "Export"
|
||||
},
|
||||
"app.export-modal.export-complete": {
|
||||
"message": "Export complete"
|
||||
},
|
||||
"app.export-modal.export-complete-description": {
|
||||
"message": "{name} was exported successfully."
|
||||
},
|
||||
"app.export-modal.header": {
|
||||
"message": "Export modpack"
|
||||
},
|
||||
|
||||
@@ -35,14 +35,14 @@ import {
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { get_project, get_search_results_v3, get_version_many } from '@/helpers/cache.js'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import {
|
||||
get_installed_project_ids as getInstalledProjectIds,
|
||||
list as listInstances,
|
||||
@@ -123,6 +123,7 @@ const {
|
||||
effectiveServerWorldId,
|
||||
serverContextServerData,
|
||||
serverContentProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
selectedServerInstallProjects,
|
||||
@@ -143,6 +144,7 @@ const {
|
||||
enforceSetupModpackRoute,
|
||||
getQueuedServerInstallPlans,
|
||||
setQueuedServerInstallPlans,
|
||||
resolveQueuedServerInstallPlan,
|
||||
openServerModpackInstallFlow,
|
||||
onServerFlowBack,
|
||||
handleServerModpackFlowCreate,
|
||||
@@ -870,6 +872,7 @@ function getCardActions(
|
||||
['modpack', 'mod', 'plugin', 'datapack'].includes(currentProjectType)
|
||||
) {
|
||||
const isQueued = queuedServerInstallProjectIds.value.has(projectResult.project_id)
|
||||
const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(projectResult.project_id)
|
||||
const isInstallingSelection = isInstallingQueuedServerInstalls.value
|
||||
const validatingInstall =
|
||||
isInstalling && currentProjectType !== 'modpack' && !isInstallingSelection
|
||||
@@ -897,14 +900,16 @@ function getCardActions(
|
||||
? CheckIcon
|
||||
: PlusIcon,
|
||||
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
|
||||
disabled: showAsInstalled || isInstalling || isInstallingSelection,
|
||||
disabled:
|
||||
showAsInstalled || isInstalling || isInstallingSelection || (isQueued && !isQueuedRoot),
|
||||
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
|
||||
type: 'outlined',
|
||||
onClick: async () => {
|
||||
if (isQueued) {
|
||||
if (isQueuedRoot) {
|
||||
removeQueuedServerInstall(projectResult.project_id)
|
||||
return
|
||||
}
|
||||
if (isQueued) return
|
||||
|
||||
const contentType = currentProjectType as BrowseInstallContentType
|
||||
const isModpack = contentType === 'modpack'
|
||||
@@ -913,7 +918,7 @@ function getCardActions(
|
||||
setProjectInstalling(projectResult.project_id, true)
|
||||
}
|
||||
try {
|
||||
await requestInstall({
|
||||
const plan = await requestInstall({
|
||||
project: projectResult,
|
||||
contentType,
|
||||
mode: isModpack ? 'immediate' : 'queue',
|
||||
@@ -937,7 +942,9 @@ function getCardActions(
|
||||
iconUrl: plan.project.icon_url ?? undefined,
|
||||
}),
|
||||
})
|
||||
if (!isModpack) await resolveQueuedServerInstallPlan(plan)
|
||||
} catch (err) {
|
||||
if (!isModpack) removeQueuedServerInstall(projectResult.project_id)
|
||||
handleError(err as Error)
|
||||
} finally {
|
||||
if (shouldShowInstalling) {
|
||||
@@ -1158,44 +1165,23 @@ if (instance.value?.game_version) {
|
||||
|
||||
void searchState.refreshSearch()
|
||||
|
||||
type UnlistenFn = () => void
|
||||
|
||||
let isUnmounted = false
|
||||
let unlistenInstances: UnlistenFn | null = null
|
||||
|
||||
onMounted(() => {
|
||||
instance_listener(async (event: { event: string; instance_id: string }) => {
|
||||
if (event.event === 'added' || event.event === 'created' || event.event === 'removed') {
|
||||
if (!route.query.i) {
|
||||
await refreshInstalledProjectIds()
|
||||
if (projectType.value === 'modpack') {
|
||||
if (event.event === 'removed') {
|
||||
syncHiddenInstanceProjectIds()
|
||||
}
|
||||
await searchState.refreshSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.value && event.instance_id === instance.value.id && event.event === 'synced') {
|
||||
useAppEvent('instance', async (event) => {
|
||||
if (event.event === 'created' || event.event === 'removed') {
|
||||
if (!route.query.i) {
|
||||
await refreshInstalledProjectIds()
|
||||
await searchState.refreshSearch()
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (isUnmounted) {
|
||||
unlisten()
|
||||
return
|
||||
if (projectType.value === 'modpack') {
|
||||
if (event.event === 'removed') {
|
||||
syncHiddenInstanceProjectIds()
|
||||
}
|
||||
await searchState.refreshSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unlistenInstances = unlisten
|
||||
})
|
||||
.catch(handleError)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isUnmounted = true
|
||||
unlistenInstances?.()
|
||||
if (instance.value && event.instance_id === instance.value.id && event.event === 'synced') {
|
||||
await refreshInstalledProjectIds()
|
||||
await searchState.refreshSearch()
|
||||
}
|
||||
})
|
||||
|
||||
function getProjectBrowseQuery() {
|
||||
|
||||
@@ -3,12 +3,12 @@ import { HomeIcon } from '@modrinth/assets'
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import type { SearchResult } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import RowDisplay from '@/components/RowDisplay.vue'
|
||||
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { get_search_results } from '@/helpers/cache.js'
|
||||
import { instance_listener } from '@/helpers/events'
|
||||
import { list } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
@@ -89,18 +89,12 @@ async function refreshFeaturedProjects() {
|
||||
await fetchInstances()
|
||||
await refreshFeaturedProjects()
|
||||
|
||||
const unlistenInstance = await instance_listener(
|
||||
async (e: { event: string; instance_id: string }) => {
|
||||
await fetchInstances()
|
||||
useAppEvent('instance', async (event) => {
|
||||
await fetchInstances()
|
||||
|
||||
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
|
||||
await refreshFeaturedProjects()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenInstance()
|
||||
if (event.event === 'created' || event.event === 'removed') {
|
||||
await refreshFeaturedProjects()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
+3
-1
@@ -32,6 +32,7 @@ import {
|
||||
} from '@/helpers/instance'
|
||||
import { get_loader_versions } from '@/helpers/metadata'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { injectAppEvents } from '@/providers/app-events'
|
||||
import { provideInstanceBackup } from '@/providers/instance-backup'
|
||||
import { useTheming } from '@/store/state'
|
||||
|
||||
@@ -41,6 +42,7 @@ import { injectInstanceSettings } from './instance-settings-context.ts'
|
||||
import SharedInstanceInstallationSettingsControls from './shared-instance-installation-settings-controls.vue'
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
const appEvents = injectAppEvents()
|
||||
const filePicker = injectFilePicker()
|
||||
const { formatMessage } = useVIntl()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -198,7 +200,7 @@ async function installLocalModpackFromPicker() {
|
||||
}).catch(handleError)
|
||||
if (!job) return false
|
||||
|
||||
const completed = await wait_for_install_job(job.job_id).catch(handleError)
|
||||
const completed = await wait_for_install_job(appEvents, job.job_id).catch(handleError)
|
||||
return !!completed
|
||||
}
|
||||
|
||||
|
||||
@@ -127,13 +127,9 @@ import { useRouter } from 'vue-router'
|
||||
import ExportModal from '@/components/ui/ExportModal.vue'
|
||||
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
|
||||
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project_versions, get_version, get_version_many } from '@/helpers/cache.js'
|
||||
import {
|
||||
instance_bulk_update_progress_listener,
|
||||
instance_listener,
|
||||
type InstanceBulkUpdateProgress,
|
||||
} from '@/helpers/events.js'
|
||||
import {
|
||||
add_project_from_path,
|
||||
edit,
|
||||
@@ -151,6 +147,7 @@ import { type InstanceContentData, loadInstanceContentData } from '@/helpers/ins
|
||||
import { get as getSettings, set as setSettings } from '@/helpers/settings'
|
||||
import type { CacheBehaviour } from '@/helpers/types'
|
||||
import { highlightModInInstance } from '@/helpers/utils.js'
|
||||
import { type AppEventPayload, injectAppEvents } from '@/providers/app-events'
|
||||
import { injectContentInstall } from '@/providers/content-install'
|
||||
import { useTheming } from '@/store/state'
|
||||
import type { FeatureFlag } from '@/store/theme'
|
||||
@@ -159,6 +156,8 @@ import { injectInstancePage } from '../instance-context'
|
||||
import { instanceContentQueryOptions, instanceKeys } from '../query-options'
|
||||
import { injectSharedInstance } from '../shared-instance-context'
|
||||
|
||||
type InstanceBulkUpdateProgress = AppEventPayload<'instance_bulk_update_progress'>
|
||||
|
||||
const messages = defineMessages({
|
||||
modpackContentHeader: {
|
||||
id: 'app.instance.content.managed-content.modpack-header',
|
||||
@@ -229,6 +228,7 @@ function contentOwnerLink(owner: ContentOwner): NonNullable<ContentOwner['link']
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const { handleError, addNotification } = injectNotificationManager()
|
||||
const appEvents = injectAppEvents()
|
||||
const { installingItems, installRevisionByInstance, installFailureRevisionByInstance } =
|
||||
injectContentInstall()
|
||||
const router = useRouter()
|
||||
@@ -955,7 +955,7 @@ async function bulkUpdateAllProjects(onProgress?: (status: BulkOperationStatus)
|
||||
message: formatMessage(messages.bulkUpdateResolvingVersions),
|
||||
waiting: true,
|
||||
})
|
||||
unlisten = await instance_bulk_update_progress_listener((progress) => {
|
||||
unlisten = appEvents.on('instance_bulk_update_progress', (progress) => {
|
||||
if (progress.instanceId !== instance.value.id) return
|
||||
onProgress(formatBulkUpdateProgress(progress))
|
||||
})
|
||||
@@ -1725,7 +1725,18 @@ const removeBeforeEach = router.beforeEach(() => {
|
||||
|
||||
let isUnmounted = false
|
||||
let unlistenDragDrop: UnlistenFn | null = null
|
||||
let unlistenInstances: UnlistenFn | null = null
|
||||
|
||||
useAppEvent('instance', async (event) => {
|
||||
if (
|
||||
instance.value &&
|
||||
event.instance_id === instance.value.id &&
|
||||
event.event === 'synced' &&
|
||||
instance.value.install_stage === 'installed' &&
|
||||
!isBulkOperating.value
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void getCurrentWebview()
|
||||
@@ -1747,27 +1758,6 @@ onMounted(() => {
|
||||
unlistenDragDrop = unlisten
|
||||
})
|
||||
.catch(handleError)
|
||||
|
||||
void instance_listener(async (event: { event: string; instance_id: string }) => {
|
||||
if (
|
||||
instance.value &&
|
||||
event.instance_id === instance.value.id &&
|
||||
event.event === 'synced' &&
|
||||
instance.value.install_stage === 'installed' &&
|
||||
!isBulkOperating.value
|
||||
) {
|
||||
await initProjects()
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (isUnmounted) {
|
||||
unlisten()
|
||||
return
|
||||
}
|
||||
|
||||
unlistenInstances = unlisten
|
||||
})
|
||||
.catch(handleError)
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -1803,6 +1793,5 @@ onUnmounted(() => {
|
||||
isUnmounted = true
|
||||
removeBeforeEach()
|
||||
unlistenDragDrop?.()
|
||||
unlistenInstances?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
writeFile as writeFileBytes,
|
||||
writeTextFile,
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { instance_listener } from '@/helpers/events'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { get_full_path } from '@/helpers/instance'
|
||||
import { highlightInFolder } from '@/helpers/utils'
|
||||
|
||||
@@ -307,20 +307,12 @@ async function handleExtractFile(path: string, override: boolean, dry: boolean)
|
||||
}
|
||||
}
|
||||
|
||||
debug('setup: registering instance_listener')
|
||||
const unlistenInstances = await instance_listener(
|
||||
async (event: { event: string; instance_id: string }) => {
|
||||
debug('instance_listener: event =', event.event, 'path =', event.instance_id)
|
||||
if (event.instance_id === instanceId.value && event.event === 'synced') {
|
||||
debug('instance_listener: synced event matched, calling refresh')
|
||||
await refresh()
|
||||
}
|
||||
},
|
||||
)
|
||||
debug('setup: instance_listener registered')
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenInstances()
|
||||
useAppEvent('instance', async (event) => {
|
||||
debug('app event: instance =', event.event, 'path =', event.instance_id)
|
||||
if (event.instance_id === instanceId.value && event.event === 'synced') {
|
||||
debug('app event: synced instance matched, calling refresh')
|
||||
await refresh()
|
||||
}
|
||||
})
|
||||
|
||||
watch(instanceId, async () => {
|
||||
|
||||
@@ -113,7 +113,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { useOnline } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, type ComputedRef, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, type ComputedRef, onUnmounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
@@ -126,9 +126,9 @@ import {
|
||||
fetchCachedServerStatus,
|
||||
getFreshCachedServerStatus,
|
||||
} from '@/composables/instances/use-server-status-query'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { instance_listener, process_listener } from '@/helpers/events'
|
||||
import {
|
||||
getSharedInstanceUnavailableReason,
|
||||
install_existing_instance,
|
||||
@@ -776,10 +776,6 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
|
||||
}
|
||||
}
|
||||
|
||||
let unlistenInstances: (() => void) | null = null
|
||||
let unlistenProcesses: (() => void) | null = null
|
||||
let instancePageAlive = true
|
||||
|
||||
provideInstancePage({
|
||||
instanceId,
|
||||
instance: instance as ComputedRef<GameInstance>,
|
||||
@@ -812,39 +808,27 @@ watch(instanceId, (currentInstanceId, previousInstanceId) => {
|
||||
destroyInstanceConsole(previousInstanceId)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void instance_listener(async (event: { instance_id: string; event: string }) => {
|
||||
if (event.instance_id !== instanceId.value) return
|
||||
if (event.event === 'removed' || route.path === '/') {
|
||||
if (route.path !== '/') await router.push({ path: '/' })
|
||||
return
|
||||
}
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: instanceKeys.detail(event.instance_id),
|
||||
exact: true,
|
||||
})
|
||||
useAppEvent('instance', async (event) => {
|
||||
if (event.instance_id !== instanceId.value) return
|
||||
if (event.event === 'removed' || route.path === '/') {
|
||||
if (route.path !== '/') await router.push({ path: '/' })
|
||||
return
|
||||
}
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: instanceKeys.detail(event.instance_id),
|
||||
exact: true,
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (instancePageAlive) unlistenInstances = unlisten
|
||||
else unlisten()
|
||||
})
|
||||
.catch(handleError)
|
||||
})
|
||||
|
||||
void process_listener((event: { event: string; instance_id: string }) => {
|
||||
if (event.instance_id !== instanceId.value) return
|
||||
if (event.event === 'finished') {
|
||||
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [])
|
||||
useInstanceConsole(event.instance_id).invalidate()
|
||||
void queryClient.invalidateQueries({ queryKey: instanceKeys.logs(event.instance_id) })
|
||||
} else if (event.event === 'launched') {
|
||||
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [true])
|
||||
}
|
||||
})
|
||||
.then((unlisten) => {
|
||||
if (instancePageAlive) unlistenProcesses = unlisten
|
||||
else unlisten()
|
||||
})
|
||||
.catch(handleError)
|
||||
useAppEvent('process', (event) => {
|
||||
if (event.instance_id !== instanceId.value) return
|
||||
if (event.event === 'finished') {
|
||||
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [])
|
||||
useInstanceConsole(event.instance_id).invalidate()
|
||||
void queryClient.invalidateQueries({ queryKey: instanceKeys.logs(event.instance_id) })
|
||||
} else if (event.event === 'launched') {
|
||||
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [true])
|
||||
}
|
||||
})
|
||||
|
||||
const icon = computed(() =>
|
||||
@@ -858,9 +842,6 @@ const timePlayed = computed(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
instancePageAlive = false
|
||||
unlistenProcesses?.()
|
||||
unlistenInstances?.()
|
||||
if (instanceId.value) {
|
||||
destroyInstanceConsole(instanceId.value)
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
provideConsoleManager,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { computed, onUnmounted, ref, shallowRef, triggerRef, watch, watchEffect } from 'vue'
|
||||
import { computed, ref, shallowRef, triggerRef, watch, watchEffect } from 'vue'
|
||||
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { useInstanceConsole } from '@/composables/useInstanceConsole'
|
||||
import { log_listener, process_listener } from '@/helpers/events.js'
|
||||
import { delete_logs_by_filename, get_output_by_filename } from '@/helpers/logs.js'
|
||||
|
||||
import { injectInstancePage } from '../instance-context'
|
||||
@@ -191,7 +191,7 @@ if (!instancePage.playing.value) {
|
||||
void analyseForCrash()
|
||||
}
|
||||
|
||||
const unlistenLog = await log_listener((payload) => {
|
||||
useAppEvent('log', (payload) => {
|
||||
if (payload.instance_id !== instanceId.value) return
|
||||
|
||||
if (payload.type === 'log4j') {
|
||||
@@ -201,7 +201,7 @@ const unlistenLog = await log_listener((payload) => {
|
||||
}
|
||||
})
|
||||
|
||||
const unlistenProcesses = await process_listener(async (e) => {
|
||||
useAppEvent('process', async (e) => {
|
||||
if (e.instance_id !== instanceId.value) return
|
||||
if (e.event === 'launched') {
|
||||
liveConsole.clear()
|
||||
@@ -216,9 +216,4 @@ const unlistenProcesses = await process_listener(async (e) => {
|
||||
void analyseForCrash()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenLog()
|
||||
unlistenProcesses()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -156,9 +156,9 @@ import ConfirmRemoveWorldModal from '@/components/ui/world/modal/ConfirmRemoveWo
|
||||
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
|
||||
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
|
||||
import WorldItem from '@/components/ui/world/WorldItem.vue'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { get_project, get_project_v3 } from '@/helpers/cache.js'
|
||||
import { instance_listener } from '@/helpers/events'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
|
||||
import {
|
||||
@@ -169,7 +169,6 @@ import {
|
||||
handleDefaultInstanceUpdateEvent,
|
||||
hasServerQuickPlaySupport,
|
||||
hasWorldQuickPlaySupport,
|
||||
type InstanceEvent,
|
||||
normalizeServerAddress,
|
||||
type ProtocolVersion,
|
||||
refreshServerData,
|
||||
@@ -399,35 +398,31 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
let unlistenInstance: (() => void) | null = null
|
||||
let worldsTabAlive = true
|
||||
|
||||
useAppEvent('instance', async (event) => {
|
||||
if (event.instance_id !== instance.value.id) return
|
||||
|
||||
console.info(`Handling instance event '${event.event}' for instance: ${event.instance_id}`)
|
||||
|
||||
if (event.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
if (isLinux) linuxRefreshCount.value++
|
||||
|
||||
await refreshAllWorlds()
|
||||
}
|
||||
|
||||
await handleDefaultInstanceUpdateEvent(worlds.value, instance.value.id, event)
|
||||
})
|
||||
|
||||
async function initWorldsTab() {
|
||||
const [_unlistenInstance, resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
instance_listener(async (e: InstanceEvent) => {
|
||||
if (e.instance_id !== instance.value.id) return
|
||||
|
||||
console.info(`Handling instance event '${e.event}' for instance: ${e.instance_id}`)
|
||||
|
||||
if (e.event === 'servers_updated') {
|
||||
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
|
||||
if (isLinux) linuxRefreshCount.value++
|
||||
|
||||
await refreshAllWorlds()
|
||||
}
|
||||
|
||||
await handleDefaultInstanceUpdateEvent(worlds.value, instance.value.id, e)
|
||||
}),
|
||||
const [resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
|
||||
get_instance_protocol_version(instance.value.id).catch(() => null),
|
||||
get_game_versions().catch(() => [] as GameVersion[]),
|
||||
])
|
||||
|
||||
if (!worldsTabAlive) {
|
||||
_unlistenInstance()
|
||||
return
|
||||
}
|
||||
if (!worldsTabAlive) return
|
||||
|
||||
unlistenInstance = _unlistenInstance
|
||||
protocolVersion.value = resolvedProtocolVersion
|
||||
gameVersions.value = resolvedGameVersions
|
||||
protocolVersionReady.value = true
|
||||
@@ -724,6 +719,5 @@ async function proceedRemoveWorld(world: World) {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
worldsTabAlive = false
|
||||
unlistenInstance?.()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { LibraryIcon, PlusIcon } from '@modrinth/assets'
|
||||
import { Button, injectNotificationManager, NavTabs } from '@modrinth/ui'
|
||||
import { inject, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import { inject, ref, shallowRef } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { NewInstanceImage } from '@/assets/icons'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import { list } from '@/helpers/instance'
|
||||
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
|
||||
|
||||
@@ -31,12 +31,9 @@ window.addEventListener('online', () => {
|
||||
offline.value = false
|
||||
})
|
||||
|
||||
const unlistenInstance = await instance_listener(async () => {
|
||||
useAppEvent('instance', async () => {
|
||||
instances.value = await list().catch(handleError)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
unlistenInstance()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -284,7 +284,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { computed, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
@@ -294,6 +294,7 @@ import {
|
||||
fetchCachedServerStatus,
|
||||
getFreshCachedServerStatus,
|
||||
} from '@/composables/instances/use-server-status-query'
|
||||
import { useAppEvent } from '@/composables/use-app-event'
|
||||
import {
|
||||
get_organization,
|
||||
get_project,
|
||||
@@ -302,7 +303,6 @@ import {
|
||||
get_version,
|
||||
get_version_many,
|
||||
} from '@/helpers/cache.js'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import {
|
||||
get as getInstance,
|
||||
get_projects as getInstanceProjects,
|
||||
@@ -814,8 +814,7 @@ function fetchDeferredServerData(project) {
|
||||
|
||||
await fetchProjectData()
|
||||
|
||||
let unlistenProcesses
|
||||
process_listener((e) => {
|
||||
useAppEvent('process', (e) => {
|
||||
if (
|
||||
e.event === 'finished' &&
|
||||
serverInstancePath.value &&
|
||||
@@ -823,12 +822,6 @@ process_listener((e) => {
|
||||
) {
|
||||
serverPlaying.value = false
|
||||
}
|
||||
}).then((unlisten) => {
|
||||
unlistenProcesses = unlisten
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProcesses?.()
|
||||
})
|
||||
|
||||
watch(
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createContext } from '@modrinth/ui'
|
||||
|
||||
import type { AppEvent } from '@/generated/app-events/AppEvent'
|
||||
|
||||
export type AppEventType = AppEvent['type']
|
||||
export type AppEventPayload<Type extends AppEventType> = Extract<
|
||||
AppEvent,
|
||||
{ type: Type }
|
||||
>['payload']
|
||||
export type AppEventHandler<Type extends AppEventType> = (
|
||||
payload: AppEventPayload<Type>,
|
||||
) => void | Promise<void>
|
||||
|
||||
export interface AppEvents {
|
||||
on<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>): () => void
|
||||
once<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>): () => void
|
||||
}
|
||||
|
||||
export const [injectAppEvents, provideAppEvents] = createContext<AppEvents>('root', 'appEvents')
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
get_team,
|
||||
get_version_many,
|
||||
} from '@/helpers/cache.js'
|
||||
import { instance_listener } from '@/helpers/events.js'
|
||||
import {
|
||||
install_create_instance,
|
||||
install_create_modpack_instance,
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
} from '@/helpers/instance'
|
||||
import { get_game_versions } from '@/helpers/tags'
|
||||
import type { GameInstance, InstanceLoader } from '@/helpers/types'
|
||||
import type { AppEvents } from '@/providers/app-events'
|
||||
import { useTheming } from '@/store/state'
|
||||
interface ModalRef {
|
||||
show: (initialVersionId?: string) => void
|
||||
@@ -60,13 +60,6 @@ type InstallingProjectDisplay = {
|
||||
organization?: string | null
|
||||
team?: string
|
||||
}
|
||||
type ContentInstallInstanceEvent = {
|
||||
event: string
|
||||
instance_id: string
|
||||
project_ids?: string[]
|
||||
message?: string
|
||||
}
|
||||
|
||||
const LOADER_ORDER = ['vanilla', 'fabric', 'quilt', 'neoforge', 'forge']
|
||||
const SUPPORTED_LOADERS: Set<string> = new Set(['vanilla', 'forge', 'fabric', 'quilt', 'neoforge'])
|
||||
const VANILLA_COMPATIBLE_LOADERS: Set<string> = new Set(['minecraft', 'datapack'])
|
||||
@@ -191,6 +184,7 @@ export const [injectContentInstall, provideContentInstall] = createContext<Conte
|
||||
export function createContentInstall(opts: {
|
||||
router: Router
|
||||
handleError: (err: unknown) => void
|
||||
appEvents: AppEvents
|
||||
}): ContentInstallContext {
|
||||
const { formatMessage } = useVIntl()
|
||||
const themeStore = useTheming()
|
||||
@@ -394,17 +388,17 @@ export function createContentInstall(opts: {
|
||||
installFailureRevisionByInstance.value = next
|
||||
}
|
||||
|
||||
void instance_listener((event: ContentInstallInstanceEvent) => {
|
||||
opts.appEvents.on('instance', (event) => {
|
||||
if (event.event === 'content_install_finished') {
|
||||
markInstanceContentChanged(event.instance_id)
|
||||
removeInstallingItems(event.instance_id, event.project_ids ?? [])
|
||||
removeInstallingItems(event.instance_id, event.project_ids)
|
||||
} else if (event.event === 'content_install_failed') {
|
||||
removeInstallingItems(event.instance_id, event.project_ids ?? [])
|
||||
removeInstallingItems(event.instance_id, event.project_ids)
|
||||
markInstanceContentInstallFailed(event.instance_id)
|
||||
markInstanceContentChanged(event.instance_id)
|
||||
opts.handleError(event.message ?? 'Failed to install content')
|
||||
opts.handleError(event.message)
|
||||
}
|
||||
}).catch(opts.handleError)
|
||||
})
|
||||
|
||||
let modalRef: ModalRef | null = null
|
||||
let modpackAlreadyInstalledModalRef: ModpackAlreadyInstalledModalRef | null = null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createContext } from '@modrinth/ui'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { loading_listener } from '@/helpers/events'
|
||||
import type { AppEvents } from '@/providers/app-events'
|
||||
|
||||
export interface AppDownloadProgressContext {
|
||||
progress: Ref<number>
|
||||
@@ -10,26 +10,19 @@ export interface AppDownloadProgressContext {
|
||||
|
||||
/* returns unlisten function */
|
||||
export async function subscribeToDownloadProgress(
|
||||
events: AppEvents,
|
||||
context: AppDownloadProgressContext,
|
||||
version: string,
|
||||
) {
|
||||
return await loading_listener(
|
||||
(event: {
|
||||
event: {
|
||||
type: 'launcher_update'
|
||||
version: string
|
||||
return events.on('loading', (event) => {
|
||||
if (event.event.type === 'launcher_update') {
|
||||
if (!version || event.event.version === version) {
|
||||
context.progress.value = event.fraction ?? 1.0
|
||||
context.version.value = event.event.version
|
||||
console.log(`Progress: ${context.progress.value} ${context.version.value}`)
|
||||
}
|
||||
fraction?: number
|
||||
}) => {
|
||||
if (event.event.type === 'launcher_update') {
|
||||
if (!version || event.event.version === version) {
|
||||
context.progress.value = event.fraction ?? 1.0
|
||||
context.version.value = event.event.version
|
||||
console.log(`Progress: ${context.progress.value} ${context.version.value}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const [injectAppUpdateDownloadProgress, provideAppUpdateDownloadProgress] =
|
||||
|
||||
@@ -17,6 +17,7 @@ import { edit, get, list } from '@/helpers/instance'
|
||||
import type { GameInstance } from '@/helpers/types'
|
||||
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
|
||||
import { start_join_server } from '@/helpers/worlds.ts'
|
||||
import type { AppEvents } from '@/providers/app-events'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -74,6 +75,7 @@ export function createServerInstall(opts: {
|
||||
router: Router
|
||||
handleError: (err: unknown) => void
|
||||
popupNotificationManager: AbstractPopupNotificationManager
|
||||
appEvents: AppEvents
|
||||
}): ServerInstallContext {
|
||||
const installingServerProjects = ref<string[]>([])
|
||||
|
||||
@@ -134,7 +136,7 @@ export function createServerInstall(opts: {
|
||||
const instanceId = installJobInstanceId(job)
|
||||
if (!instanceId) return null
|
||||
|
||||
await wait_for_install_job(job.job_id)
|
||||
await wait_for_install_job(opts.appEvents, job.job_id)
|
||||
await ensureManagedServerWorldExists(instanceId, project.title, serverAddress)
|
||||
|
||||
return instanceId
|
||||
@@ -145,7 +147,7 @@ export function createServerInstall(opts: {
|
||||
|
||||
await edit(instance.id, { game_version: targetGameVersion })
|
||||
const job = await install_existing_instance(instance.id, false)
|
||||
await wait_for_install_job(job.job_id)
|
||||
await wait_for_install_job(opts.appEvents, job.job_id)
|
||||
}
|
||||
|
||||
function showModpackInstallSuccess(project: GameInstance, serverAddress: string | null) {
|
||||
@@ -261,7 +263,7 @@ export function createServerInstall(opts: {
|
||||
const instanceId = installJobInstanceId(createJob)
|
||||
if (!instanceId) return
|
||||
|
||||
await wait_for_install_job(createJob.job_id)
|
||||
await wait_for_install_job(opts.appEvents, createJob.job_id)
|
||||
await ensureManagedServerWorldExists(instanceId, project.title, serverAddress)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import type { AppEvent } from '@/generated/app-events/AppEvent'
|
||||
import { deserialize } from '@/generated/app-events/postcard'
|
||||
|
||||
type WireObject = Record<string, unknown>
|
||||
|
||||
interface WireEnum {
|
||||
tag: string
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
function wireObject(value: unknown): WireObject {
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new TypeError('Invalid Postcard app event object')
|
||||
}
|
||||
return value as WireObject
|
||||
}
|
||||
|
||||
function wireEnum(value: unknown): WireEnum {
|
||||
const event = wireObject(value)
|
||||
if (typeof event.tag !== 'string') {
|
||||
throw new TypeError('Invalid Postcard app event enum')
|
||||
}
|
||||
return event as unknown as WireEnum
|
||||
}
|
||||
|
||||
function taggedObject(value: unknown, tag: string): WireObject {
|
||||
const event = wireEnum(value)
|
||||
return {
|
||||
[tag]: event.tag,
|
||||
...(event.value === undefined ? {} : wireObject(event.value)),
|
||||
}
|
||||
}
|
||||
|
||||
function unitVariant(value: unknown): string {
|
||||
return wireEnum(value).tag
|
||||
}
|
||||
|
||||
function nullable(value: unknown): unknown {
|
||||
return value === undefined ? null : value
|
||||
}
|
||||
|
||||
function number(value: unknown): unknown {
|
||||
return typeof value === 'bigint' ? Number(value) : value
|
||||
}
|
||||
|
||||
function normalizeLoadingPayload(value: unknown): WireObject {
|
||||
const payload = wireObject(value)
|
||||
const event = taggedObject(payload.event, 'type')
|
||||
|
||||
for (const field of ['icon', 'pack_id', 'pack_version']) {
|
||||
if (field in event) event[field] = nullable(event[field])
|
||||
}
|
||||
|
||||
return {
|
||||
...payload,
|
||||
event,
|
||||
fraction: nullable(payload.fraction),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProcessPayload(value: unknown): WireObject {
|
||||
const payload = wireObject(value)
|
||||
return { ...payload, event: unitVariant(payload.event) }
|
||||
}
|
||||
|
||||
function normalizeInstancePayload(value: unknown): WireObject {
|
||||
const payload = wireObject(value)
|
||||
const event = wireEnum(payload.event)
|
||||
return {
|
||||
instance_id: payload.instance_id,
|
||||
event: event.tag,
|
||||
...(event.value === undefined ? {} : wireObject(event.value)),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBulkUpdatePayload(value: unknown): WireObject {
|
||||
const payload = wireObject(value)
|
||||
return {
|
||||
...payload,
|
||||
stage: unitVariant(payload.stage),
|
||||
current: number(payload.current),
|
||||
total: number(payload.total),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCommandPayload(value: unknown): WireObject {
|
||||
const command = taggedObject(value, 'event')
|
||||
if (command.event === 'LaunchInstance') {
|
||||
command.server = nullable(command.server)
|
||||
command.singleplayer_world = nullable(command.singleplayer_world)
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
function normalizeFriendPayload(value: unknown): WireObject {
|
||||
const event = taggedObject(value, 'event')
|
||||
if (event.event === 'status_update') {
|
||||
const status = wireObject(event.user_status)
|
||||
event.user_status = {
|
||||
...status,
|
||||
profile_name: nullable(status.profile_name),
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
function normalizeLogPayload(value: unknown): WireObject {
|
||||
const payload = wireObject(value)
|
||||
const event = taggedObject(payload.event, 'type')
|
||||
|
||||
if (event.type === 'log4j') {
|
||||
for (const field of [
|
||||
'timestamp_millis',
|
||||
'logger_name',
|
||||
'level',
|
||||
'thread_name',
|
||||
'message',
|
||||
'throwable',
|
||||
]) {
|
||||
event[field] = nullable(event[field])
|
||||
}
|
||||
event.timestamp_millis = number(event.timestamp_millis)
|
||||
}
|
||||
|
||||
return { instance_id: payload.instance_id, ...event }
|
||||
}
|
||||
|
||||
function normalizeInstallProgress(value: unknown): WireObject {
|
||||
const progress = wireObject(value)
|
||||
const secondary = progress.secondary
|
||||
|
||||
return {
|
||||
...progress,
|
||||
current: number(progress.current),
|
||||
total: number(progress.total),
|
||||
secondary:
|
||||
secondary === undefined
|
||||
? undefined
|
||||
: {
|
||||
...wireObject(secondary),
|
||||
current: number(wireObject(secondary).current),
|
||||
total: number(wireObject(secondary).total),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeInstallDetails(value: unknown): WireObject {
|
||||
const details = taggedObject(value, 'type')
|
||||
|
||||
if (details.type === 'minecraft') details.loader = unitVariant(details.loader)
|
||||
if (details.type === 'java') details.step = unitVariant(details.step)
|
||||
if (details.type === 'import') details.launcher_type = unitVariant(details.launcher_type)
|
||||
if (details.type === 'modpack') {
|
||||
details.project_id = nullable(details.project_id)
|
||||
details.version_id = nullable(details.version_id)
|
||||
details.title = nullable(details.title)
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
function normalizeInstallContext(value: unknown): WireObject {
|
||||
const context = wireObject(value)
|
||||
return {
|
||||
...context,
|
||||
expected_size: context.expected_size === undefined ? undefined : number(context.expected_size),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeInstallError(value: unknown): WireObject {
|
||||
const error = wireObject(value)
|
||||
return {
|
||||
...error,
|
||||
phase: error.phase === undefined ? undefined : unitVariant(error.phase),
|
||||
reason: error.reason === undefined ? undefined : unitVariant(error.reason),
|
||||
api: error.api === undefined ? undefined : wireObject(error.api),
|
||||
context: error.context === undefined ? undefined : normalizeInstallContext(error.context),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeInstallJob(value: unknown): WireObject {
|
||||
const job = wireObject(value)
|
||||
const display = job.display === undefined ? null : wireObject(job.display)
|
||||
|
||||
if (display !== null) display.icon = nullable(display.icon)
|
||||
|
||||
return {
|
||||
...job,
|
||||
instance_id: nullable(job.instance_id),
|
||||
kind: unitVariant(job.kind),
|
||||
status: unitVariant(job.status),
|
||||
target: (() => {
|
||||
const target = taggedObject(job.target, 'type')
|
||||
if ('instance_id' in target) target.instance_id = nullable(target.instance_id)
|
||||
return target
|
||||
})(),
|
||||
phase: unitVariant(job.phase),
|
||||
progress: job.progress === undefined ? null : normalizeInstallProgress(job.progress),
|
||||
details: normalizeInstallDetails(job.details),
|
||||
display,
|
||||
error: job.error === undefined ? null : normalizeInstallError(job.error),
|
||||
rollback_error:
|
||||
job.rollback_error === undefined ? null : normalizeInstallError(job.rollback_error),
|
||||
finished: nullable(job.finished),
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeAppEvent(payload: ArrayBuffer): AppEvent {
|
||||
const result = deserialize('AppEvent', new Uint8Array(payload))
|
||||
if (result.bytes.length !== 0) {
|
||||
throw new TypeError('Postcard app event contained trailing bytes')
|
||||
}
|
||||
const event = result.value as WireEnum
|
||||
|
||||
const decoded = (() => {
|
||||
switch (event.tag) {
|
||||
case 'loading':
|
||||
return { type: event.tag, payload: normalizeLoadingPayload(event.value) }
|
||||
case 'process':
|
||||
return { type: event.tag, payload: normalizeProcessPayload(event.value) }
|
||||
case 'instance':
|
||||
return { type: event.tag, payload: normalizeInstancePayload(event.value) }
|
||||
case 'instance_bulk_update_progress':
|
||||
return { type: event.tag, payload: normalizeBulkUpdatePayload(event.value) }
|
||||
case 'install_job':
|
||||
return { type: event.tag, payload: normalizeInstallJob(event.value) }
|
||||
case 'command':
|
||||
return { type: event.tag, payload: normalizeCommandPayload(event.value) }
|
||||
case 'warning':
|
||||
return { type: event.tag, payload: wireObject(event.value) }
|
||||
case 'friend':
|
||||
return { type: event.tag, payload: normalizeFriendPayload(event.value) }
|
||||
case 'notification':
|
||||
return { type: event.tag, payload: JSON.parse(String(event.value)) as unknown }
|
||||
case 'log':
|
||||
return { type: event.tag, payload: normalizeLogPayload(event.value) }
|
||||
case 'ads_consent_required':
|
||||
return { type: event.tag, payload: event.value }
|
||||
default:
|
||||
throw new TypeError(`Unknown Postcard app event: ${event.tag}`)
|
||||
}
|
||||
})()
|
||||
|
||||
return decoded as AppEvent
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Channel } from '@tauri-apps/api/core'
|
||||
import { onScopeDispose } from 'vue'
|
||||
|
||||
import {
|
||||
type AppEventHandler,
|
||||
type AppEvents,
|
||||
type AppEventType,
|
||||
provideAppEvents,
|
||||
} from '@/providers/app-events'
|
||||
import { decodeAppEvent } from '@/providers/setup/app-event-codec'
|
||||
|
||||
type UntypedAppEventHandler = (payload: unknown) => void | Promise<void>
|
||||
|
||||
export function setupAppEventsProvider() {
|
||||
const handlers = new Map<AppEventType, Set<UntypedAppEventHandler>>()
|
||||
|
||||
function on<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>) {
|
||||
let eventHandlers = handlers.get(type)
|
||||
if (!eventHandlers) {
|
||||
eventHandlers = new Set()
|
||||
handlers.set(type, eventHandlers)
|
||||
}
|
||||
|
||||
const untypedHandler = handler as unknown as UntypedAppEventHandler
|
||||
eventHandlers.add(untypedHandler)
|
||||
return () => {
|
||||
eventHandlers.delete(untypedHandler)
|
||||
if (eventHandlers.size === 0) handlers.delete(type)
|
||||
}
|
||||
}
|
||||
|
||||
function once<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>) {
|
||||
let unsubscribe = () => {}
|
||||
unsubscribe = on(type, async (payload) => {
|
||||
unsubscribe()
|
||||
await handler(payload)
|
||||
})
|
||||
return unsubscribe
|
||||
}
|
||||
|
||||
const events: AppEvents = { on, once }
|
||||
const channel = new Channel<ArrayBuffer>((payload) => {
|
||||
const event = decodeAppEvent(payload)
|
||||
const eventHandlers = handlers.get(event.type)
|
||||
if (!eventHandlers) return
|
||||
|
||||
for (const handler of [...eventHandlers]) {
|
||||
void Promise.resolve()
|
||||
.then(() => handler(event.payload))
|
||||
.catch((error) => {
|
||||
console.error(`Unhandled ${event.type} app event`, error)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
provideAppEvents(events)
|
||||
|
||||
function dispose() {
|
||||
handlers.clear()
|
||||
}
|
||||
|
||||
onScopeDispose(dispose)
|
||||
|
||||
return { channel, events, dispose }
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import type { AbstractModrinthClient, Archon, Labrinth } from '@modrinth/api-client'
|
||||
import type { Archon, Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
addPendingServerContentInstalls,
|
||||
type BrowseInstallPlan,
|
||||
type BrowseSelectedProject,
|
||||
createContext,
|
||||
type CreationFlowContextValue,
|
||||
flushStoredServerAddonInstallQueue,
|
||||
getServerAddonInstallPlanProjectIds,
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
type ModpackSearchResult,
|
||||
type PendingServerContentInstall,
|
||||
type PendingServerContentInstallType,
|
||||
readPendingServerContentInstalls,
|
||||
readStoredServerInstallQueue,
|
||||
removePendingServerContentInstall,
|
||||
writePendingServerContentInstallBaseline,
|
||||
resolveServerAddonInstallPlans,
|
||||
useServerContextRuntime,
|
||||
useServerPanelSync,
|
||||
waitForServerContextRuntimeReady,
|
||||
writeStoredServerInstallQueue,
|
||||
} from '@modrinth/ui'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
@@ -28,7 +28,6 @@ type InstallableSearchResult = Labrinth.Search.v3.ResultSearchProject & {
|
||||
installing?: boolean
|
||||
installed?: boolean
|
||||
}
|
||||
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
|
||||
|
||||
export interface ServerModpackSelectionRequest {
|
||||
projectId: string
|
||||
@@ -54,6 +53,7 @@ export interface ServerInstallContentContext {
|
||||
effectiveServerWorldId: ComputedRef<string | null>
|
||||
serverContextServerData: Ref<Archon.Servers.v0.Server | null>
|
||||
serverContentProjectIds: Ref<Set<string>>
|
||||
queuedServerInstallRootProjectIds: ComputedRef<Set<string>>
|
||||
queuedServerInstallProjectIds: ComputedRef<Set<string>>
|
||||
queuedServerInstallCount: ComputedRef<number>
|
||||
selectedServerInstallProjects: ComputedRef<BrowseSelectedProject[]>
|
||||
@@ -76,6 +76,9 @@ export interface ServerInstallContentContext {
|
||||
setQueuedServerInstallPlans: (
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) => void
|
||||
resolveQueuedServerInstallPlan: (
|
||||
plan: BrowseInstallPlan<InstallableSearchResult>,
|
||||
) => Promise<void>
|
||||
openServerModpackInstallFlow: (request: ServerModpackSelectionRequest) => Promise<void>
|
||||
onServerFlowBack: () => void
|
||||
handleServerModpackFlowCreate: (config: CreationFlowContextValue) => Promise<void>
|
||||
@@ -90,114 +93,6 @@ function readQueryString(value: unknown): string | null {
|
||||
return typeof value === 'string' && value.length > 0 ? value : null
|
||||
}
|
||||
|
||||
function getQueuedInstallOwnerFallback(project: InstallableSearchResult) {
|
||||
if (project.organization) {
|
||||
const ownerId = project.organization_id ?? project.organization
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.organization,
|
||||
type: 'organization' as const,
|
||||
link: `https://modrinth.com/organization/${ownerId}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.author) return null
|
||||
|
||||
const ownerId = project.author_id ?? project.author
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.author,
|
||||
type: 'user' as const,
|
||||
link: `/user/${encodeURIComponent(ownerId)}`,
|
||||
}
|
||||
}
|
||||
|
||||
async function getQueuedInstallOwner(
|
||||
client: AbstractModrinthClient,
|
||||
project: InstallableSearchResult,
|
||||
) {
|
||||
const fallback = getQueuedInstallOwnerFallback(project)
|
||||
|
||||
try {
|
||||
if (project.organization) {
|
||||
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
|
||||
if (organization) {
|
||||
return {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
type: 'organization' as const,
|
||||
avatar_url: organization.icon_url ?? undefined,
|
||||
link: `https://modrinth.com/organization/${organization.slug}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
|
||||
const owner =
|
||||
members.find((member) => member.user.id === project.author_id)?.user ??
|
||||
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
|
||||
members[0]?.user
|
||||
|
||||
if (owner) {
|
||||
return {
|
||||
id: owner.id,
|
||||
name: owner.username,
|
||||
type: 'user' as const,
|
||||
avatar_url: owner.avatar_url,
|
||||
link: `/user/${encodeURIComponent(owner.username)}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getQueuedAddonInstallPlans(
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholder(
|
||||
plan: BrowseInstallPlan<InstallableSearchResult>,
|
||||
owner: PendingServerContentInstallInput['owner'],
|
||||
): PendingServerContentInstallInput {
|
||||
const project = plan.project as InstallableSearchResult & { slug?: string | null }
|
||||
return {
|
||||
projectId: plan.projectId,
|
||||
versionId: plan.versionId,
|
||||
contentType: plan.contentType as PendingServerContentInstallType,
|
||||
title: project.name ?? 'Project',
|
||||
versionName: plan.versionName ?? null,
|
||||
versionNumber: plan.versionNumber ?? null,
|
||||
fileName: plan.fileName ?? null,
|
||||
owner,
|
||||
slug: project.slug ?? plan.projectId,
|
||||
iconUrl: project.icon_url ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholderFallbacks(
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
return getQueuedAddonInstallPlans(plans).map((plan) =>
|
||||
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
|
||||
)
|
||||
}
|
||||
|
||||
async function getQueuedInstallPlaceholders(
|
||||
client: AbstractModrinthClient,
|
||||
plans: Map<string, BrowseInstallPlan<InstallableSearchResult>>,
|
||||
) {
|
||||
return Promise.all(
|
||||
getQueuedAddonInstallPlans(plans).map(async (plan) =>
|
||||
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(client, plan.project)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function createServerInstallContent(opts: {
|
||||
serverSetupModalRef: Ref<ServerSetupModalHandle | null>
|
||||
}) {
|
||||
@@ -220,6 +115,7 @@ export function createServerInstallContent(opts: {
|
||||
const isFromWorlds = computed(() => browseFrom.value === 'worlds')
|
||||
const isServerContext = computed(() => !!serverIdQuery.value)
|
||||
const isSetupServerContext = computed(() => !!serverIdQuery.value && !!serverFlowFrom.value)
|
||||
useServerContextRuntime(serverIdQuery)
|
||||
|
||||
const serverContextWorldId = ref<string | null>(worldIdQuery.value)
|
||||
|
||||
@@ -246,11 +142,15 @@ export function createServerInstallContent(opts: {
|
||||
initialServerId ? getCachedServer(initialServerId) : null,
|
||||
)
|
||||
const serverContentProjectIds = ref<Set<string>>(new Set())
|
||||
const serverContentInstallKeys = ref<Set<string>>(new Set())
|
||||
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<InstallableSearchResult>>>(
|
||||
new Map(),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
||||
const queuedServerInstallRootProjectIds = computed(
|
||||
() => new Set(queuedServerInstalls.value.keys()),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() =>
|
||||
getServerAddonInstallPlanProjectIds(queuedServerInstalls.value.values()),
|
||||
)
|
||||
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
||||
const selectedServerInstallProjects = computed<BrowseSelectedProject[]>(() =>
|
||||
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
|
||||
@@ -262,6 +162,10 @@ export function createServerInstallContent(opts: {
|
||||
const isInstallingQueuedServerInstalls = ref(false)
|
||||
const queuedInstallProgress = ref({ completed: 0, total: 0 })
|
||||
const effectiveServerWorldId = computed(() => worldIdQuery.value ?? serverContextWorldId.value)
|
||||
useServerPanelSync({
|
||||
serverId: serverIdQuery,
|
||||
worldId: effectiveServerWorldId,
|
||||
})
|
||||
const serverBackUrl = computed(() => {
|
||||
const sid = serverIdQuery.value
|
||||
if (!sid) return '/hosting/manage'
|
||||
@@ -307,11 +211,7 @@ export function createServerInstallContent(opts: {
|
||||
.map((addon) => addon.project_id)
|
||||
.filter((projectId): projectId is string => !!projectId),
|
||||
)
|
||||
const keys = new Set(
|
||||
(content.addons ?? []).map((addon) => addon.project_id ?? addon.filename),
|
||||
)
|
||||
serverContentProjectIds.value = ids
|
||||
serverContentInstallKeys.value = keys
|
||||
} catch (err) {
|
||||
handleError(err as Error)
|
||||
}
|
||||
@@ -353,14 +253,12 @@ export function createServerInstallContent(opts: {
|
||||
serverContextWorldId.value = null
|
||||
serverContextServerData.value = null
|
||||
serverContentProjectIds.value = new Set()
|
||||
serverContentInstallKeys.value = new Set()
|
||||
setQueuedServerInstallPlans(new Map())
|
||||
return
|
||||
}
|
||||
|
||||
if (sid !== prevSid) {
|
||||
serverContentProjectIds.value = new Set()
|
||||
serverContentInstallKeys.value = new Set()
|
||||
queuedServerInstalls.value = readStoredServerInstallQueue(sid, wid)
|
||||
serverContextServerData.value = getCachedServer(sid)
|
||||
try {
|
||||
@@ -462,6 +360,59 @@ export function createServerInstallContent(opts: {
|
||||
writeStoredServerInstallQueue(serverId, worldId, plans)
|
||||
}
|
||||
|
||||
function toResolvePreferences(
|
||||
preferences?: BrowseInstallPlan<InstallableSearchResult>['preferences'],
|
||||
): Labrinth.Content.v3.ResolutionPreferences {
|
||||
return {
|
||||
game_versions: preferences?.gameVersions,
|
||||
loaders: preferences?.loaders,
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAddonPlan(
|
||||
plan: BrowseInstallPlan<InstallableSearchResult>,
|
||||
existingProjectIds: string[],
|
||||
) {
|
||||
const target = getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: serverContextServerData.value?.mc_version,
|
||||
loader: serverContextServerData.value?.loader,
|
||||
},
|
||||
plan.contentType,
|
||||
)
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(target),
|
||||
existing_project_ids: existingProjectIds,
|
||||
})
|
||||
|
||||
return [resolved.primary, ...resolved.dependencies].map((item) => ({
|
||||
projectId: item.project_id,
|
||||
versionId: item.version_id,
|
||||
}))
|
||||
}
|
||||
|
||||
async function resolveQueuedServerInstallPlan(plan: BrowseInstallPlan<InstallableSearchResult>) {
|
||||
const resolvedContent = await resolveAddonPlan(plan, Array.from(serverContentProjectIds.value))
|
||||
const storedPlan = queuedServerInstalls.value.get(plan.projectId)
|
||||
if (!storedPlan || storedPlan.versionId !== plan.versionId) return
|
||||
|
||||
const nextPlans = new Map(queuedServerInstalls.value)
|
||||
nextPlans.set(plan.projectId, { ...storedPlan, resolvedContent })
|
||||
setQueuedServerInstallPlans(nextPlans)
|
||||
}
|
||||
|
||||
async function resolveQueuedAddonPlans(plans: BrowseInstallPlan<InstallableSearchResult>[]) {
|
||||
return await resolveServerAddonInstallPlans({
|
||||
plans,
|
||||
existingProjectIds: serverContentProjectIds.value,
|
||||
resolvePlan: resolveAddonPlan,
|
||||
})
|
||||
}
|
||||
|
||||
async function flushQueuedServerInstalls(
|
||||
serverId: string | null = serverIdQuery.value,
|
||||
worldId: string | null = effectiveServerWorldId.value,
|
||||
@@ -476,6 +427,13 @@ export function createServerInstallContent(opts: {
|
||||
const queuedPlans = getStoredServerAddonInstallQueue<InstallableSearchResult>(serverId, worldId)
|
||||
if (queuedPlans.size === 0) return true
|
||||
|
||||
try {
|
||||
await waitForServerContextRuntimeReady(client, serverId)
|
||||
} catch (error) {
|
||||
handleError(error as Error)
|
||||
return false
|
||||
}
|
||||
|
||||
isInstallingQueuedServerInstalls.value = true
|
||||
queuedInstallProgress.value = {
|
||||
completed: 0,
|
||||
@@ -486,22 +444,16 @@ export function createServerInstallContent(opts: {
|
||||
const result = await flushStoredServerAddonInstallQueue({
|
||||
serverId,
|
||||
worldId,
|
||||
install: (plans) =>
|
||||
client.archon.content_v1.addAddons(
|
||||
serverId,
|
||||
worldId,
|
||||
plans.map((plan) => ({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
})),
|
||||
),
|
||||
install: async (plans) => {
|
||||
const addons = await resolveQueuedAddonPlans(plans)
|
||||
if (addons.length > 0) {
|
||||
await client.archon.content_v1.addAddons(serverId, worldId, addons)
|
||||
}
|
||||
},
|
||||
onQueueChange: (plans) => setStoredServerInstallPlans(serverId, worldId, plans),
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
for (const plan of result.attemptedPlans) {
|
||||
removePendingServerContentInstall(serverId, worldId, plan.projectId)
|
||||
}
|
||||
handleError(result.error as Error)
|
||||
return false
|
||||
}
|
||||
@@ -514,10 +466,6 @@ export function createServerInstallContent(opts: {
|
||||
...serverContentProjectIds.value,
|
||||
...result.flushedPlans.map((plan) => plan.projectId),
|
||||
])
|
||||
serverContentInstallKeys.value = new Set([
|
||||
...serverContentInstallKeys.value,
|
||||
...result.flushedPlans.map((plan) => plan.projectId),
|
||||
])
|
||||
if (result.flushedPlans.length > 0) {
|
||||
await queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] })
|
||||
}
|
||||
@@ -542,23 +490,10 @@ export function createServerInstallContent(opts: {
|
||||
|
||||
if (sid && wid) {
|
||||
writeStoredServerInstallQueue(sid, wid, plans)
|
||||
writePendingServerContentInstallBaseline(sid, wid, serverContentInstallKeys.value)
|
||||
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
|
||||
void getQueuedInstallPlaceholders(client, plans)
|
||||
.then((items) => {
|
||||
const pendingProjectIds = new Set(
|
||||
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
|
||||
)
|
||||
addPendingServerContentInstalls(
|
||||
sid,
|
||||
wid,
|
||||
items.filter((item) => pendingProjectIds.has(item.projectId)),
|
||||
)
|
||||
})
|
||||
.catch((err) => handleError(err as Error))
|
||||
}
|
||||
const installed = await flushQueuedServerInstalls(sid, wid)
|
||||
if (!installed) return false
|
||||
await router.push(backUrl)
|
||||
void flushQueuedServerInstalls(sid, wid)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -627,6 +562,7 @@ export function createServerInstallContent(opts: {
|
||||
effectiveServerWorldId,
|
||||
serverContextServerData,
|
||||
serverContentProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
selectedServerInstallProjects,
|
||||
@@ -647,6 +583,7 @@ export function createServerInstallContent(opts: {
|
||||
enforceSetupModpackRoute,
|
||||
getQueuedServerInstallPlans,
|
||||
setQueuedServerInstallPlans,
|
||||
resolveQueuedServerInstallPlan,
|
||||
openServerModpackInstallFlow,
|
||||
onServerFlowBack,
|
||||
handleServerModpackFlowCreate,
|
||||
|
||||
@@ -77,6 +77,7 @@ default = ["custom-protocol"]
|
||||
# this feature is used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
export-app-events = ["theseus/export-ts"]
|
||||
updater = []
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"scripts": {
|
||||
"tauri": "tauri",
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev",
|
||||
"dev": "tauri dev --features export-app-events",
|
||||
"test": "cargo nextest run --all-targets --no-fail-fast",
|
||||
"lint": "cargo fmt --check && cargo clippy --all-targets && cargo clippy --all-targets --features updater",
|
||||
"lint:ancillary": "prettier --check .",
|
||||
|
||||
+13
-9
@@ -3,9 +3,9 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use tauri::plugin::TauriPlugin;
|
||||
use tauri::{Emitter, Manager, PhysicalPosition, PhysicalSize, Runtime};
|
||||
use tauri::{Manager, PhysicalPosition, PhysicalSize, Runtime};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use theseus::settings;
|
||||
use theseus::{AppEvent, EventState, settings};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub struct AdsState {
|
||||
@@ -20,7 +20,6 @@ pub struct AdsState {
|
||||
}
|
||||
|
||||
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
|
||||
const ADS_CONSENT_REQUIRED_EVENT: &str = "ads-consent-required";
|
||||
const APP_TITLE_BAR_HEIGHT: f32 = 48.0;
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
pub(super) const OCCLUDED_AREA_THRESHOLD: f64 = 0.5;
|
||||
@@ -38,6 +37,12 @@ const ADS_USER_AGENT: &str = concat!(
|
||||
" (Modrinth App)",
|
||||
);
|
||||
|
||||
fn emit_ads_consent_required(required: bool) {
|
||||
EventState::get()
|
||||
.send(AppEvent::AdsConsentRequired(required))
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn ads_user_agent_override_params() -> String {
|
||||
serde_json::json!({
|
||||
@@ -666,7 +671,7 @@ pub async fn init_ads_window<R: Runtime>(
|
||||
&& state.consent_required
|
||||
&& state.consent_notification_enabled
|
||||
{
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
|
||||
emit_ads_consent_required(true);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -704,7 +709,7 @@ pub async fn update_ads_window_hold<R: Runtime>(
|
||||
&& state.consent_required
|
||||
&& state.consent_notification_enabled
|
||||
{
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
|
||||
emit_ads_consent_required(true);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -730,7 +735,7 @@ pub async fn hide_ads_window<R: Runtime>(
|
||||
}
|
||||
|
||||
if reset {
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, false).ok();
|
||||
emit_ads_consent_required(false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -761,8 +766,7 @@ pub async fn show_ads_consent_ui<R: Runtime>(
|
||||
)?;
|
||||
}
|
||||
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, show_notification)
|
||||
.ok();
|
||||
emit_ads_consent_required(show_notification);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -841,7 +845,7 @@ pub async fn finish_ads_consent_flow<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, false).ok();
|
||||
emit_ads_consent_required(false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+12
-2
@@ -25,9 +25,12 @@ mod updater_impl_noop;
|
||||
// Should be called in launcher initialization
|
||||
#[tracing::instrument(skip_all)]
|
||||
#[tauri::command]
|
||||
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||
async fn initialize_state(
|
||||
app: tauri::AppHandle,
|
||||
events: tauri::ipc::Channel<tauri::ipc::InvokeResponseBody>,
|
||||
) -> api::Result<()> {
|
||||
tracing::info!("Initializing app event state...");
|
||||
theseus::EventState::init(app.clone()).await?;
|
||||
theseus::EventState::init(app.clone(), events).await?;
|
||||
|
||||
tracing::info!("Initializing app state...");
|
||||
State::init(app.config().identifier.clone()).await?;
|
||||
@@ -111,6 +114,13 @@ async fn set_restart_after_pending_update(
|
||||
// if Tauri app is called with arguments, then those arguments will be treated as commands
|
||||
// ie: deep links or filepaths for .mrpacks
|
||||
fn main() {
|
||||
#[cfg(feature = "export-app-events")]
|
||||
theseus::export_app_event_bindings(
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../app-frontend/src/generated/app-events"),
|
||||
)
|
||||
.expect("failed to export app event TypeScript bindings");
|
||||
|
||||
/*
|
||||
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
|
||||
ERROR > WARN > INFO > DEBUG > TRACE
|
||||
|
||||
@@ -15,12 +15,9 @@ import { I18nDebugPanel, LoadingBar, NotificationPanel } from '@modrinth/ui'
|
||||
|
||||
import AdsConsentNotification from '~/components/ui/AdsConsentNotification.vue'
|
||||
import { setupProviders } from '~/providers/setup.ts'
|
||||
import { setupUserCountryProvider } from '~/providers/setup/user-country.ts'
|
||||
|
||||
import { useAuth } from './composables/auth'
|
||||
|
||||
setupUserCountryProvider()
|
||||
|
||||
const auth = await useAuth()
|
||||
setupProviders(auth)
|
||||
</script>
|
||||
|
||||
@@ -400,10 +400,10 @@ defineExpose({ show, hide })
|
||||
</Combobox>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.deleteAllGroups)"
|
||||
type="quiet"
|
||||
color="red"
|
||||
type="base"
|
||||
:label="formatMessage(messages.deleteAllGroups)"
|
||||
:disabled="titleButtonsDisabled"
|
||||
class="[&:not(:disabled):focus-visible>svg]:!text-red [&:not(:disabled):hover>svg]:!text-red"
|
||||
@click="showConfirmClearGroups"
|
||||
>
|
||||
<TrashIcon v-if="!isClearing" aria-hidden="true" />
|
||||
|
||||
@@ -6,26 +6,25 @@ import type {
|
||||
CreationFlowContextValue,
|
||||
EnvironmentSearchOverride,
|
||||
FilterValue,
|
||||
PendingServerContentInstall,
|
||||
PendingServerContentInstallType,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
addPendingServerContentInstalls,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
flushStoredServerAddonInstallQueue,
|
||||
getServerAddonInstallPlanProjectIds,
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
injectModrinthClient,
|
||||
injectNotificationManager,
|
||||
readPendingServerContentInstalls,
|
||||
readStoredServerInstallQueue,
|
||||
removePendingServerContentInstall,
|
||||
requestInstall,
|
||||
resolveServerAddonInstallPlans,
|
||||
stripServerRuntimeInstallFilters,
|
||||
stripServerRuntimeInstallOverrides,
|
||||
useServerContextRuntime,
|
||||
useServerPanelSync,
|
||||
useVIntl,
|
||||
writePendingServerContentInstallBaseline,
|
||||
waitForServerContextRuntimeReady,
|
||||
writeStoredServerInstallQueue,
|
||||
} from '@modrinth/ui'
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
@@ -35,7 +34,6 @@ import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { navigateTo, useRoute } from '#app'
|
||||
import { queryAsString } from '~/utils/router'
|
||||
|
||||
type PendingServerContentInstallInput = Omit<PendingServerContentInstall, 'createdAt'>
|
||||
type ServerInstallBrowseSearchState = Pick<
|
||||
BrowseSearchState,
|
||||
'currentFilters' | 'overriddenProvidedFilterTypes'
|
||||
@@ -88,34 +86,6 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
function getQueuedInstallOwnerFallback(project: ServerInstallSearchResult) {
|
||||
if (project.organization) {
|
||||
const ownerId = project.organization_id ?? project.organization
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.organization,
|
||||
type: 'organization' as const,
|
||||
link: `/organization/${ownerId}`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!project.author) return null
|
||||
|
||||
const ownerId = project.author_id ?? project.author
|
||||
return {
|
||||
id: ownerId,
|
||||
name: project.author,
|
||||
type: 'user' as const,
|
||||
link: `/user/${ownerId}`,
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuedAddonInstallPlans(
|
||||
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
|
||||
) {
|
||||
return Array.from(plans.values()).filter((plan) => plan.contentType !== 'modpack')
|
||||
}
|
||||
|
||||
export function useServerInstallContent({
|
||||
projectType,
|
||||
onboardingModalRef,
|
||||
@@ -136,6 +106,11 @@ export function useServerInstallContent({
|
||||
const currentServerId = computed(() => queryAsString(route.query.sid) || null)
|
||||
const fromContext = computed(() => queryAsString(route.query.from) || null)
|
||||
const currentWorldId = computed(() => queryAsString(route.query.wid) || null)
|
||||
useServerContextRuntime(currentServerId)
|
||||
useServerPanelSync({
|
||||
serverId: currentServerId,
|
||||
worldId: currentWorldId,
|
||||
})
|
||||
|
||||
const {
|
||||
data: serverData,
|
||||
@@ -177,7 +152,12 @@ export function useServerInstallContent({
|
||||
const queuedServerInstalls = ref<Map<string, BrowseInstallPlan<ServerInstallSearchResult>>>(
|
||||
readStoredServerInstallQueue(currentServerId.value, currentWorldId.value),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() => new Set(queuedServerInstalls.value.keys()))
|
||||
const queuedServerInstallRootProjectIds = computed(
|
||||
() => new Set(queuedServerInstalls.value.keys()),
|
||||
)
|
||||
const queuedServerInstallProjectIds = computed(() =>
|
||||
getServerAddonInstallPlanProjectIds(queuedServerInstalls.value.values()),
|
||||
)
|
||||
const queuedServerInstallCount = computed(() => queuedServerInstalls.value.size)
|
||||
const selectedServerInstallProjects = computed(() =>
|
||||
Array.from(queuedServerInstalls.value.values()).map((plan) => ({
|
||||
@@ -219,81 +199,6 @@ export function useServerInstallContent({
|
||||
writeStoredServerInstallQueue(serverId, worldId, plans)
|
||||
}
|
||||
|
||||
async function getQueuedInstallOwner(project: ServerInstallSearchResult) {
|
||||
const fallback = getQueuedInstallOwnerFallback(project)
|
||||
|
||||
try {
|
||||
if (project.organization) {
|
||||
const organization = await client.labrinth.projects_v3.getOrganization(project.project_id)
|
||||
if (organization) {
|
||||
return {
|
||||
id: organization.id,
|
||||
name: organization.name,
|
||||
type: 'organization' as const,
|
||||
avatar_url: organization.icon_url ?? undefined,
|
||||
link: `/organization/${organization.slug}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const members = await client.labrinth.projects_v3.getMembers(project.project_id)
|
||||
const owner =
|
||||
members.find((member) => member.user.id === project.author_id)?.user ??
|
||||
members.find((member) => member.is_owner || member.role === 'Owner')?.user ??
|
||||
members[0]?.user
|
||||
|
||||
if (owner) {
|
||||
return {
|
||||
id: owner.id,
|
||||
name: owner.username,
|
||||
type: 'user' as const,
|
||||
avatar_url: owner.avatar_url,
|
||||
link: `/user/${owner.username}`,
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholder(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
owner: PendingServerContentInstallInput['owner'],
|
||||
): PendingServerContentInstallInput {
|
||||
return {
|
||||
projectId: plan.projectId,
|
||||
versionId: plan.versionId,
|
||||
contentType: plan.contentType as PendingServerContentInstallType,
|
||||
title: getInstallProjectName(plan.project),
|
||||
versionName: plan.versionName ?? null,
|
||||
versionNumber: plan.versionNumber ?? null,
|
||||
fileName: plan.fileName ?? null,
|
||||
owner,
|
||||
slug: plan.project.slug ?? plan.projectId,
|
||||
iconUrl: plan.project.icon_url ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuedInstallPlaceholderFallbacks(
|
||||
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
|
||||
) {
|
||||
return getQueuedAddonInstallPlans(plans).map((plan) =>
|
||||
getQueuedInstallPlaceholder(plan, getQueuedInstallOwnerFallback(plan.project)),
|
||||
)
|
||||
}
|
||||
|
||||
async function getQueuedInstallPlaceholders(
|
||||
plans: Map<string, BrowseInstallPlan<ServerInstallSearchResult>>,
|
||||
) {
|
||||
return Promise.all(
|
||||
getQueuedAddonInstallPlans(plans).map(async (plan) =>
|
||||
getQueuedInstallPlaceholder(plan, await getQueuedInstallOwner(plan.project)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function setProjectInstalling(projectId: string, installing: boolean) {
|
||||
const next = new Set(installingProjectIds.value)
|
||||
if (installing) {
|
||||
@@ -319,10 +224,6 @@ export function useServerInstallContent({
|
||||
)
|
||||
}
|
||||
|
||||
function getServerInstalledContentKeys(data = serverContentData.value) {
|
||||
return new Set((data?.addons ?? []).map((addon) => addon.project_id ?? addon.filename))
|
||||
}
|
||||
|
||||
function syncHiddenInstalledProjectIds() {
|
||||
hiddenInstalledProjectIds.value = new Set([
|
||||
...getServerInstalledProjectIds(),
|
||||
@@ -437,32 +338,43 @@ export function useServerInstallContent({
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAddonPlan(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
existingProjectIds: string[],
|
||||
) {
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(getServerInstallTargetPreferences(plan.contentType)),
|
||||
existing_project_ids: existingProjectIds,
|
||||
})
|
||||
|
||||
return [resolved.primary, ...resolved.dependencies].map((item) => ({
|
||||
projectId: item.project_id,
|
||||
versionId: item.version_id,
|
||||
}))
|
||||
}
|
||||
|
||||
async function resolveAndStoreQueuedAddonPlan(
|
||||
plan: BrowseInstallPlan<ServerInstallSearchResult>,
|
||||
) {
|
||||
const resolvedContent = await resolveAddonPlan(plan, Array.from(getServerInstalledProjectIds()))
|
||||
const storedPlan = queuedServerInstalls.value.get(plan.projectId)
|
||||
if (!storedPlan || storedPlan.versionId !== plan.versionId) return
|
||||
|
||||
const nextPlans = new Map(queuedServerInstalls.value)
|
||||
nextPlans.set(plan.projectId, { ...storedPlan, resolvedContent })
|
||||
serverInstallQueue.set(nextPlans)
|
||||
}
|
||||
|
||||
async function resolveQueuedAddonPlans(plans: BrowseInstallPlan<ServerInstallSearchResult>[]) {
|
||||
const existingProjectIds = getServerInstalledProjectIds()
|
||||
const resolvedAddons: Array<{ project_id: string; version_id: string }> = []
|
||||
|
||||
for (const plan of plans) {
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(getServerInstallTargetPreferences(plan.contentType)),
|
||||
existing_project_ids: Array.from(existingProjectIds),
|
||||
})
|
||||
const content = [resolved.primary, ...resolved.dependencies]
|
||||
|
||||
for (const item of content) {
|
||||
if (existingProjectIds.has(item.project_id)) continue
|
||||
existingProjectIds.add(item.project_id)
|
||||
resolvedAddons.push({
|
||||
project_id: item.project_id,
|
||||
version_id: item.version_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedAddons
|
||||
return await resolveServerAddonInstallPlans({
|
||||
plans,
|
||||
existingProjectIds: getServerInstalledProjectIds(),
|
||||
resolvePlan: resolveAddonPlan,
|
||||
})
|
||||
}
|
||||
|
||||
function getInstallProjectVersions(projectId: string) {
|
||||
@@ -498,6 +410,13 @@ export function useServerInstallContent({
|
||||
)
|
||||
if (queuedPlans.size === 0) return true
|
||||
|
||||
try {
|
||||
await waitForServerContextRuntimeReady(client, serverId)
|
||||
} catch (error) {
|
||||
handleError(error as Error)
|
||||
return false
|
||||
}
|
||||
|
||||
isInstallingQueuedServerInstalls.value = true
|
||||
queuedInstallProgress.value = {
|
||||
completed: 0,
|
||||
@@ -518,9 +437,6 @@ export function useServerInstallContent({
|
||||
})
|
||||
|
||||
if (!result.ok) {
|
||||
for (const plan of result.attemptedPlans) {
|
||||
removePendingServerContentInstall(serverId, worldId, plan.projectId)
|
||||
}
|
||||
handleError(result.error as Error)
|
||||
return false
|
||||
}
|
||||
@@ -533,10 +449,7 @@ export function useServerInstallContent({
|
||||
total: result.flushedPlans.length,
|
||||
}
|
||||
if (result.flushedPlans.length > 0) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list', 'v1', serverId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] }),
|
||||
])
|
||||
await queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -559,26 +472,10 @@ export function useServerInstallContent({
|
||||
|
||||
if (sid && wid) {
|
||||
writeStoredServerInstallQueue(sid, wid, plans)
|
||||
writePendingServerContentInstallBaseline(sid, wid, [
|
||||
...getServerInstalledContentKeys(),
|
||||
...optimisticallyInstalledProjectIds.value,
|
||||
])
|
||||
addPendingServerContentInstalls(sid, wid, getQueuedInstallPlaceholderFallbacks(plans))
|
||||
void getQueuedInstallPlaceholders(plans)
|
||||
.then((items) => {
|
||||
const pendingProjectIds = new Set(
|
||||
readPendingServerContentInstalls(sid, wid).map((item) => item.projectId),
|
||||
)
|
||||
addPendingServerContentInstalls(
|
||||
sid,
|
||||
wid,
|
||||
items.filter((item) => pendingProjectIds.has(item.projectId)),
|
||||
)
|
||||
})
|
||||
.catch((err) => handleError(err as Error))
|
||||
}
|
||||
const installed = await flushQueuedServerInstalls(sid, wid)
|
||||
if (!installed) return false
|
||||
await navigateTo(backUrl)
|
||||
void flushQueuedServerInstalls(sid, wid)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -598,16 +495,17 @@ export function useServerInstallContent({
|
||||
const isModpack = contentType === 'modpack'
|
||||
|
||||
try {
|
||||
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) {
|
||||
if (!isModpack && queuedServerInstallRootProjectIds.value.has(project.project_id)) {
|
||||
removeQueuedServerInstall(project.project_id)
|
||||
return
|
||||
}
|
||||
if (!isModpack && queuedServerInstallProjectIds.value.has(project.project_id)) return
|
||||
|
||||
if (isModpack || !queuedServerInstallProjectIds.value.has(project.project_id)) {
|
||||
setProjectInstalling(project.project_id, true)
|
||||
}
|
||||
|
||||
await requestInstall({
|
||||
const plan = await requestInstall({
|
||||
project,
|
||||
contentType,
|
||||
mode: isModpack ? 'immediate' : 'queue',
|
||||
@@ -646,10 +544,13 @@ export function useServerInstallContent({
|
||||
ctx.modal.value?.setStage('final-config')
|
||||
},
|
||||
})
|
||||
if (!isModpack) await resolveAndStoreQueuedAddonPlan(plan)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (isModpack) {
|
||||
setProjectInstalling(project.project_id, false)
|
||||
} else {
|
||||
removeQueuedServerInstall(project.project_id)
|
||||
}
|
||||
handleError(e instanceof Error ? e : new Error(`Error installing content ${e}`))
|
||||
} finally {
|
||||
@@ -804,6 +705,7 @@ export function useServerInstallContent({
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
isInstallingQueuedServerInstalls,
|
||||
|
||||
@@ -97,39 +97,16 @@ import {
|
||||
LoadingBar,
|
||||
normalizeChildren,
|
||||
NotificationPanel,
|
||||
provideModrinthClient,
|
||||
provideNotificationManager,
|
||||
providePageContext,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
|
||||
import Logo404 from '~/assets/images/404.svg'
|
||||
import { getSignInRouteObj } from '~/composables/auth.js'
|
||||
import { logout } from '~/composables/user.js'
|
||||
|
||||
import { createModrinthClient } from './helpers/api.ts'
|
||||
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
|
||||
import { setupLoadingStateProvider } from './providers/setup/loading-state.ts'
|
||||
import { setupProviders } from '~/providers/setup.ts'
|
||||
|
||||
const auth = await useAuth()
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
provideNotificationManager(new FrontendNotificationManager())
|
||||
setupLoadingStateProvider()
|
||||
|
||||
const client = createModrinthClient(auth.value, {
|
||||
apiBaseUrl: config.public.apiBaseUrl.replace('/v2/', '/'),
|
||||
archonBaseUrl: config.public.pyroBaseUrl.replace('/v2/', '/'),
|
||||
sharedInstancesBaseUrl: config.public.sharedInstancesBaseUrl,
|
||||
rateLimitKey: config.rateLimitKey,
|
||||
})
|
||||
provideModrinthClient(client)
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(false),
|
||||
showAds: ref(false),
|
||||
adConsentAvailable: ref(false),
|
||||
openExternalUrl: (url) => window.open(url, '_blank'),
|
||||
})
|
||||
setupProviders(auth)
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
|
||||
@@ -3458,6 +3458,12 @@
|
||||
"project.actions.back-to-project-page": {
|
||||
"message": "Back to project page"
|
||||
},
|
||||
"project.actions.check-modpack-archives": {
|
||||
"message": "Check modpack unzip"
|
||||
},
|
||||
"project.actions.checking-modpack-archives": {
|
||||
"message": "Checking..."
|
||||
},
|
||||
"project.actions.create-server": {
|
||||
"message": "Create a server"
|
||||
},
|
||||
@@ -3743,9 +3749,24 @@
|
||||
"project.moderation.title": {
|
||||
"message": "Moderation"
|
||||
},
|
||||
"project.modpack-archive-warning.description": {
|
||||
"message": "Importing this .mrpack might be broken."
|
||||
},
|
||||
"project.modpack-archive-warning.title": {
|
||||
"message": "This modpack was published during export bug"
|
||||
},
|
||||
"project.navigation.changelog": {
|
||||
"message": "Changelog"
|
||||
},
|
||||
"project.notification.check-modpack-archives.failed": {
|
||||
"message": "Some modpack files could not be unzipped"
|
||||
},
|
||||
"project.notification.check-modpack-archives.no-files": {
|
||||
"message": "No .mrpack files were found for this project."
|
||||
},
|
||||
"project.notification.check-modpack-archives.success": {
|
||||
"message": "{count, plural, one {The modpack file can be unzipped.} other {All # modpack files can be unzipped.}}"
|
||||
},
|
||||
"project.notification.icon-updated.message": {
|
||||
"message": "Your project's icon has been updated."
|
||||
},
|
||||
|
||||
@@ -405,6 +405,36 @@
|
||||
:auth="auth"
|
||||
:tags="tags"
|
||||
/>
|
||||
<Admonition
|
||||
v-if="
|
||||
auth.user &&
|
||||
tags.staffRoles.includes(auth.user.role) &&
|
||||
project.actualProjectType === 'modpack' &&
|
||||
hasModpackArchiveInWarningWindow
|
||||
"
|
||||
type="warning"
|
||||
:header="formatMessage(messages.modpackArchiveWarningTitle)"
|
||||
class="mt-3"
|
||||
>
|
||||
{{ formatMessage(messages.modpackArchiveWarningDescription) }}
|
||||
<template #actions>
|
||||
<Button
|
||||
type="colored"
|
||||
color="orange"
|
||||
:loading="isCheckingModpackArchives"
|
||||
@click="checkModpackArchives"
|
||||
>
|
||||
<FileArchiveIcon />
|
||||
{{
|
||||
formatMessage(
|
||||
isCheckingModpackArchives
|
||||
? messages.checkingModpackArchives
|
||||
: messages.checkModpackArchives,
|
||||
)
|
||||
}}
|
||||
</Button>
|
||||
</template>
|
||||
</Admonition>
|
||||
<Admonition
|
||||
v-if="
|
||||
currentMember &&
|
||||
@@ -529,6 +559,7 @@ import {
|
||||
ClipboardCopyIcon,
|
||||
CompassIcon,
|
||||
DownloadIcon,
|
||||
FileArchiveIcon,
|
||||
FolderSearchIcon,
|
||||
HeartIcon,
|
||||
LeftArrowIcon,
|
||||
@@ -663,6 +694,7 @@ const downloadModal = ref()
|
||||
const openInAppModal = ref()
|
||||
const overTheTopDownloadAnimation = ref()
|
||||
const scanModal = ref()
|
||||
const isCheckingModpackArchives = ref(false)
|
||||
|
||||
const projectV3Loaded = computed(() => !projectV3Pending.value || projectV3.value != null)
|
||||
const isServerProject = computed(() => projectV3.value?.minecraft_server != null)
|
||||
@@ -818,6 +850,35 @@ const messages = defineMessages({
|
||||
id: 'project.actions.rescan-modpack',
|
||||
defaultMessage: 'Rescan modpack',
|
||||
},
|
||||
checkModpackArchives: {
|
||||
id: 'project.actions.check-modpack-archives',
|
||||
defaultMessage: 'Check modpack unzip',
|
||||
},
|
||||
checkingModpackArchives: {
|
||||
id: 'project.actions.checking-modpack-archives',
|
||||
defaultMessage: 'Checking...',
|
||||
},
|
||||
checkModpackArchivesSuccess: {
|
||||
id: 'project.notification.check-modpack-archives.success',
|
||||
defaultMessage:
|
||||
'{count, plural, one {The modpack file can be unzipped.} other {All # modpack files can be unzipped.}}',
|
||||
},
|
||||
checkModpackArchivesFailed: {
|
||||
id: 'project.notification.check-modpack-archives.failed',
|
||||
defaultMessage: 'Some modpack files could not be unzipped',
|
||||
},
|
||||
checkModpackArchivesNoFiles: {
|
||||
id: 'project.notification.check-modpack-archives.no-files',
|
||||
defaultMessage: 'No .mrpack files were found for this project.',
|
||||
},
|
||||
modpackArchiveWarningTitle: {
|
||||
id: 'project.modpack-archive-warning.title',
|
||||
defaultMessage: 'This modpack was published during export bug',
|
||||
},
|
||||
modpackArchiveWarningDescription: {
|
||||
id: 'project.modpack-archive-warning.description',
|
||||
defaultMessage: 'Importing this .mrpack might be broken.',
|
||||
},
|
||||
serversPromoDescription: {
|
||||
id: 'project.actions.servers-promo.description',
|
||||
defaultMessage: 'Modrinth Hosting is the easiest way to play with your friends without hassle!',
|
||||
@@ -1135,7 +1196,7 @@ const {
|
||||
|
||||
const dependencies = computed(() => dependenciesRaw.value ?? null)
|
||||
|
||||
// V3 Versions - lazy loaded client-side only
|
||||
// V3 Versions - lazy loaded client-side only (except for staff, who need v3 versions for moderation)
|
||||
const versionsEnabled = ref(false)
|
||||
const {
|
||||
data: versionsV3,
|
||||
@@ -1149,7 +1210,7 @@ const {
|
||||
apiVersion: 3,
|
||||
}),
|
||||
staleTime: STALE_TIME_LONG,
|
||||
enabled: computed(() => !!projectId.value && versionsEnabled.value),
|
||||
enabled: computed(() => !!projectId.value && (versionsEnabled.value || isStaff(auth.value.user))),
|
||||
})
|
||||
|
||||
// Organization
|
||||
@@ -1749,6 +1810,90 @@ const showProjectHeaderCreateServerAction = computed(
|
||||
const projectHeaderCreateServerTo = computed(() =>
|
||||
project.value ? `/hosting?project=${project.value.id}#plan` : '/hosting',
|
||||
)
|
||||
|
||||
const MRPACK_ARCHIVE_WARNING_START = new Date('2026-08-10T17:00:00.000Z').getTime()
|
||||
const MRPACK_ARCHIVE_WARNING_END = new Date('2026-08-13T20:00:00.000Z').getTime()
|
||||
const hasModpackArchiveInWarningWindow = computed(() =>
|
||||
(versionsV3.value ?? []).some((version) => {
|
||||
const publishedAt = new Date(version.date_published).getTime()
|
||||
return (
|
||||
version.files.some((file) => file.filename.toLowerCase().endsWith('.mrpack')) &&
|
||||
publishedAt >= MRPACK_ARCHIVE_WARNING_START &&
|
||||
publishedAt <= MRPACK_ARCHIVE_WARNING_END
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
async function checkModpackArchives() {
|
||||
if (!project.value || isCheckingModpackArchives.value) return
|
||||
|
||||
isCheckingModpackArchives.value = true
|
||||
startLoading()
|
||||
|
||||
try {
|
||||
const versions = await client.labrinth.versions_v2.getProjectVersions(project.value.id)
|
||||
const filesByUrl = new Map(
|
||||
versions
|
||||
.flatMap((version) => version.files)
|
||||
.filter((file) => file.filename.toLowerCase().endsWith('.mrpack'))
|
||||
.map((file) => [file.url, file]),
|
||||
)
|
||||
const files = [...filesByUrl.values()]
|
||||
|
||||
if (files.length === 0) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: formatMessage(messages.checkModpackArchivesNoFiles),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { default: JSZip } = await import('jszip')
|
||||
const failures = []
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const response = await fetch(file.url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed (${response.status} ${response.statusText})`)
|
||||
}
|
||||
|
||||
await JSZip.loadAsync(await response.blob(), { checkCRC32: true })
|
||||
} catch (error) {
|
||||
failures.push({
|
||||
filename: file.filename,
|
||||
error: error?.message ?? String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.checkModpackArchivesFailed),
|
||||
text: failures.map((failure) => `${failure.filename}: ${failure.error}`).join('\n'),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.successLabel),
|
||||
text: formatMessage(messages.checkModpackArchivesSuccess, { count: files.length }),
|
||||
type: 'success',
|
||||
})
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
||||
text: error?.data?.description ?? error?.message ?? String(error),
|
||||
type: 'error',
|
||||
})
|
||||
} finally {
|
||||
isCheckingModpackArchives.value = false
|
||||
stopLoading()
|
||||
}
|
||||
}
|
||||
|
||||
const projectHeaderMoreActions = computed(() => {
|
||||
const isStaff = !!(auth.value.user && tags.value.staffRoles.includes(auth.value.user.role))
|
||||
|
||||
@@ -1787,6 +1932,19 @@ const projectHeaderMoreActions = computed(() => {
|
||||
tone: 'orange',
|
||||
shown: !!auth.value.user && isStaff && project.value?.actualProjectType === 'modpack',
|
||||
},
|
||||
{
|
||||
id: 'moderation-modpack-check-archives',
|
||||
label: formatMessage(
|
||||
isCheckingModpackArchives.value
|
||||
? messages.checkingModpackArchives
|
||||
: messages.checkModpackArchives,
|
||||
),
|
||||
icon: FileArchiveIcon,
|
||||
action: checkModpackArchives,
|
||||
tone: 'orange',
|
||||
disabled: isCheckingModpackArchives.value,
|
||||
shown: !!auth.value.user && isStaff && project.value?.actualProjectType === 'modpack',
|
||||
},
|
||||
{ type: 'divider', shown: !!auth.value.user && isStaff },
|
||||
{
|
||||
id: 'report',
|
||||
|
||||
@@ -163,6 +163,7 @@ const {
|
||||
hideSelectedServerInstalls,
|
||||
installingProjectIds,
|
||||
optimisticallyInstalledProjectIds,
|
||||
queuedServerInstallRootProjectIds,
|
||||
queuedServerInstallProjectIds,
|
||||
queuedServerInstallCount,
|
||||
isInstallingQueuedServerInstalls,
|
||||
@@ -327,6 +328,7 @@ function getCardActions(
|
||||
|
||||
if (serverData.value) {
|
||||
const isQueued = queuedServerInstallProjectIds.value.has(result.project_id)
|
||||
const isQueuedRoot = queuedServerInstallRootProjectIds.value.has(result.project_id)
|
||||
const isInstalled =
|
||||
projectResult.installed ||
|
||||
optimisticallyInstalledProjectIds.value.has(result.project_id) ||
|
||||
@@ -362,7 +364,8 @@ function getCardActions(
|
||||
? CheckIcon
|
||||
: DownloadIcon,
|
||||
iconClass: isInstalling || isInstallingSelection ? 'animate-spin' : undefined,
|
||||
disabled: !!isInstalled || isInstalling || isInstallingSelection,
|
||||
disabled:
|
||||
!!isInstalled || isInstalling || isInstallingSelection || (isQueued && !isQueuedRoot),
|
||||
color: isQueued && !isInstalling && !isInstallingSelection ? 'green' : 'brand',
|
||||
type: 'outlined',
|
||||
onClick: () => serverInstall(projectResult),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="markdown-body">
|
||||
<h1>Content Rules</h1>
|
||||
<p><em>Last modified: August 12, 2026</em></p>
|
||||
<p><em>Last modified: August 13, 2026</em></p>
|
||||
|
||||
<p>
|
||||
These Content Rules are to be considered part of our
|
||||
|
||||
@@ -7,6 +7,7 @@ import { setupLoadingStateProvider } from './setup/loading-state'
|
||||
import { setupModrinthClientProvider } from './setup/modrinth-client'
|
||||
import { setupPageContextProvider } from './setup/page-context'
|
||||
import { setupTagsProvider } from './setup/tags'
|
||||
import { setupUserCountryProvider } from './setup/user-country'
|
||||
|
||||
export function setupProviders(auth: Awaited<ReturnType<typeof useAuth>>) {
|
||||
provideNotificationManager(new FrontendNotificationManager())
|
||||
@@ -17,4 +18,5 @@ export function setupProviders(auth: Awaited<ReturnType<typeof useAuth>>) {
|
||||
setupFilePickerProvider()
|
||||
setupPageContextProvider()
|
||||
setupLoadingStateProvider()
|
||||
setupUserCountryProvider()
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -318,6 +318,7 @@ vars! {
|
||||
NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL;
|
||||
|
||||
EMAIL_DOMAIN_BLACKLIST: StringCsv = StringCsv(vec![]);
|
||||
EMAIL_DOMAIN_WHITELIST: StringCsv = StringCsv(vec![]);
|
||||
|
||||
CLICKHOUSE_REPLICATED: bool = false;
|
||||
CLICKHOUSE_URL: String = "http://localhost:8123";
|
||||
|
||||
@@ -1847,9 +1847,35 @@ impl From<NewAccount> for AccountRegisterFlow {
|
||||
}
|
||||
}
|
||||
|
||||
/// The bundled disposable domain list is checked first. Environment blacklist
|
||||
/// entries are matched literally, unless they begin with `*.`, in which case
|
||||
/// they match any subdomain of the remaining suffix.
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum EmailDomainStatus {
|
||||
Whitelisted,
|
||||
Neutral,
|
||||
}
|
||||
|
||||
/// Environment list entries are matched literally, unless they begin with `*.`,
|
||||
/// in which case they match any subdomain of the remaining suffix.
|
||||
fn matches_domain_entry(domain: &str, entry: &str) -> bool {
|
||||
let entry = entry.trim().to_ascii_lowercase();
|
||||
|
||||
match entry.strip_prefix("*.") {
|
||||
Some(suffix) => domain
|
||||
.strip_suffix(suffix)
|
||||
.is_some_and(|subdomain| subdomain.ends_with('.')),
|
||||
None => entry == domain,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_whitelisted_domain(domain: &str) -> bool {
|
||||
let domain = domain.to_ascii_lowercase();
|
||||
|
||||
ENV.EMAIL_DOMAIN_WHITELIST
|
||||
.iter()
|
||||
.any(|entry| matches_domain_entry(&domain, entry))
|
||||
}
|
||||
|
||||
/// The bundled disposable domain list is checked first, then the environment
|
||||
/// blacklist.
|
||||
fn is_blacklisted_domain(domain: &str) -> bool {
|
||||
let domain = domain.to_ascii_lowercase();
|
||||
|
||||
@@ -1863,35 +1889,39 @@ fn is_blacklisted_domain(domain: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
ENV.EMAIL_DOMAIN_BLACKLIST.iter().any(|entry| {
|
||||
let entry = entry.trim().to_ascii_lowercase();
|
||||
|
||||
match entry.strip_prefix("*.") {
|
||||
Some(suffix) => domain
|
||||
.strip_suffix(suffix)
|
||||
.is_some_and(|subdomain| subdomain.ends_with('.')),
|
||||
None => entry == domain,
|
||||
}
|
||||
})
|
||||
ENV.EMAIL_DOMAIN_BLACKLIST
|
||||
.iter()
|
||||
.any(|entry| matches_domain_entry(&domain, entry))
|
||||
}
|
||||
|
||||
fn ensure_email_domain_is_allowed(email: &str) -> Result<(), ApiError> {
|
||||
fn ensure_email_domain_is_allowed(
|
||||
email: &str,
|
||||
) -> Result<EmailDomainStatus, ApiError> {
|
||||
let Some((_, domain)) = email.rsplit_once('@') else {
|
||||
return Err(ApiError::Request(email_check_error_generic()));
|
||||
};
|
||||
|
||||
if is_whitelisted_domain(domain) {
|
||||
info!(email.domain = domain, "whitelisted email domain, allowing");
|
||||
return Ok(EmailDomainStatus::Whitelisted);
|
||||
}
|
||||
|
||||
if is_blacklisted_domain(domain) {
|
||||
info!(email.domain = domain, "blacklisted email domain, denying");
|
||||
return Err(ApiError::Request(email_check_error_generic()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(EmailDomainStatus::Neutral)
|
||||
}
|
||||
|
||||
async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> {
|
||||
ensure_email_domain_is_allowed(email)
|
||||
let status = ensure_email_domain_is_allowed(email)
|
||||
.wrap_api_err("validating email domain is allowed")?;
|
||||
|
||||
if status == EmailDomainStatus::Whitelisted {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let result = check_email(email)
|
||||
.await
|
||||
.wrap_request_err("checking email address")?;
|
||||
|
||||
@@ -232,6 +232,13 @@ pub async fn paypal_webhook(
|
||||
None,
|
||||
)
|
||||
.await
|
||||
// Make deserialization errors yield a 5xx instead of a 400. For
|
||||
// webhooks, the server must respect the client's request schema rather
|
||||
// than requiring the client to conform to the server's schema.
|
||||
.map_err(|error| match error {
|
||||
ApiError::Request(report) => ApiError::Internal(report),
|
||||
error => error,
|
||||
})
|
||||
.wrap_api_err("verifying PayPal webhook signature")?;
|
||||
|
||||
if &webhook_res.verification_status != "SUCCESS" {
|
||||
@@ -242,7 +249,7 @@ pub async fn paypal_webhook(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PayPalResource {
|
||||
pub payout_item_id: String,
|
||||
pub payout_item_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -252,7 +259,7 @@ pub async fn paypal_webhook(
|
||||
}
|
||||
|
||||
let webhook = serde_json::from_str::<PayPalWebhook>(&body)
|
||||
.wrap_request_err("deserializing JSON data")?;
|
||||
.wrap_internal_err("deserializing PayPal webhook JSON data")?;
|
||||
|
||||
match &*webhook.event_type {
|
||||
"PAYMENT.PAYOUTS-ITEM.BLOCKED"
|
||||
@@ -260,6 +267,13 @@ pub async fn paypal_webhook(
|
||||
| "PAYMENT.PAYOUTS-ITEM.REFUNDED"
|
||||
| "PAYMENT.PAYOUTS-ITEM.RETURNED"
|
||||
| "PAYMENT.PAYOUTS-ITEM.CANCELED" => {
|
||||
let payout_item_id = webhook
|
||||
.resource
|
||||
.payout_item_id
|
||||
.as_deref()
|
||||
.wrap_internal_err(
|
||||
"PayPal payout item webhook is missing `payout_item_id`",
|
||||
)?;
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
@@ -267,7 +281,7 @@ pub async fn paypal_webhook(
|
||||
|
||||
let result = sqlx::query!(
|
||||
"SELECT user_id, amount, fee FROM payouts WHERE platform_id = $1 AND status = $2",
|
||||
webhook.resource.payout_item_id,
|
||||
payout_item_id,
|
||||
PayoutStatus::InTransit.as_str()
|
||||
)
|
||||
.fetch_optional(&mut transaction)
|
||||
@@ -286,7 +300,7 @@ pub async fn paypal_webhook(
|
||||
PayoutStatus::Failed
|
||||
}
|
||||
.as_str(),
|
||||
webhook.resource.payout_item_id
|
||||
payout_item_id
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
@@ -309,6 +323,13 @@ pub async fn paypal_webhook(
|
||||
}
|
||||
}
|
||||
"PAYMENT.PAYOUTS-ITEM.SUCCEEDED" => {
|
||||
let payout_item_id = webhook
|
||||
.resource
|
||||
.payout_item_id
|
||||
.as_deref()
|
||||
.wrap_internal_err(
|
||||
"PayPal payout item webhook is missing `payout_item_id`",
|
||||
)?;
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
@@ -320,7 +341,7 @@ pub async fn paypal_webhook(
|
||||
WHERE platform_id = $2
|
||||
",
|
||||
PayoutStatus::Success.as_str(),
|
||||
webhook.resource.payout_item_id
|
||||
payout_item_id
|
||||
)
|
||||
.execute(&mut transaction)
|
||||
.await
|
||||
|
||||
@@ -9,6 +9,7 @@ export type WebSocketEventHandler<
|
||||
export interface WebSocketConnection {
|
||||
serverId: string
|
||||
socket: WebSocket
|
||||
authenticated: boolean
|
||||
reconnectAttempts: number
|
||||
reconnectTimer?: ReturnType<typeof setTimeout>
|
||||
isReconnecting: boolean
|
||||
@@ -31,6 +32,7 @@ export abstract class AbstractWebSocketClient {
|
||||
protected readonly MAX_RECONNECT_ATTEMPTS = 10
|
||||
protected readonly RECONNECT_BASE_DELAY = 1000
|
||||
protected readonly RECONNECT_MAX_DELAY = 30000
|
||||
protected readonly AUTHENTICATION_TIMEOUT = 30000
|
||||
|
||||
constructor(
|
||||
protected client: {
|
||||
@@ -58,6 +60,7 @@ export abstract class AbstractWebSocketClient {
|
||||
}
|
||||
|
||||
if (status && !status.connected && !options?.force) {
|
||||
await this.waitForAuthentication(serverId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -69,6 +72,28 @@ export abstract class AbstractWebSocketClient {
|
||||
await this.connect(serverId, auth)
|
||||
}
|
||||
|
||||
protected async waitForAuthentication(serverId: string): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let unsubscribe = () => {}
|
||||
const timeout = setTimeout(() => {
|
||||
unsubscribe()
|
||||
reject(new Error(`WebSocket authentication timed out for server ${serverId}`))
|
||||
}, this.AUTHENTICATION_TIMEOUT)
|
||||
|
||||
unsubscribe = this.on(serverId, 'auth-ok', () => {
|
||||
clearTimeout(timeout)
|
||||
unsubscribe()
|
||||
resolve()
|
||||
})
|
||||
|
||||
if (this.getStatus(serverId)?.connected) {
|
||||
clearTimeout(timeout)
|
||||
unsubscribe()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
on<E extends Archon.Websocket.v0.WSEventType>(
|
||||
serverId: string,
|
||||
eventType: E,
|
||||
@@ -88,7 +113,7 @@ export abstract class AbstractWebSocketClient {
|
||||
if (!connection) return null
|
||||
|
||||
return {
|
||||
connected: connection.socket.readyState === WebSocket.OPEN,
|
||||
connected: connection.socket.readyState === WebSocket.OPEN && connection.authenticated,
|
||||
reconnecting: connection.isReconnecting,
|
||||
reconnectAttempts: connection.reconnectAttempts,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user