mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
refactor: app event bus (#6985)
* refactor: app event bus * fix: use messagepack + cleanup * fix: use postcard * fix: lint * fix: import gate * fix: prettierignore + lint * fix: lint * fix: rev
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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": "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": "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: "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: "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,953 @@
|
||||
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: "minecraft_download",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 6:
|
||||
return {
|
||||
tag: "instance_update",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 7:
|
||||
return {
|
||||
tag: "zip_extract",
|
||||
value: {
|
||||
instance_id: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 8:
|
||||
return {
|
||||
tag: "config_change",
|
||||
value: {
|
||||
new_path: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 9:
|
||||
return {
|
||||
tag: "copy_instance",
|
||||
value: {
|
||||
import_location: d.deserialize_string(),
|
||||
instance_name: d.deserialize_string()
|
||||
}
|
||||
};
|
||||
case 10:
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -1158,44 +1158,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 }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user