import { proxy, useSnapshot } from 'valtio' import { useEffect, useRef, useState } from 'react' import { activeModalStack, activeModalStacks, hideModal, insertActiveModalStack, miscUiState } from '../globalState' import { guessProblem } from '../errorLoadingScreenHelpers' import type { ConnectOptions } from '../connect' import { downloadPacketsReplay, packetsRecordingState, replayLogger } from '../packetsReplay/packetsReplayLegacy' import { getProxyDetails } from '../microsoftAuthflow' import { downloadAutoCapturedPackets, getLastAutoCapturedPackets } from '../mineflayer/plugins/packetsRecording' import { appQueryParams } from '../appParams' import AppStatus from './AppStatus' import DiveTransition from './DiveTransition' import { useDidUpdateEffect } from './utils' import { useIsModalActive } from './utilsApp' import Button from './Button' import { updateAuthenticatedAccountData, updateLoadedServerData, AuthenticatedAccount } from './serversStorage' import { showOptionsModal } from './SelectOption' import LoadingChunks from './LoadingChunks' import MessageFormattedString from './MessageFormattedString' const initialState = { status: '', lastStatus: '', maybeRecoverable: true, descriptionHint: '', isError: false, hideDots: false, loadingChunksData: null as null | Record, loadingChunksDataPlayerChunk: null as null | { x: number, z: number }, isDisplaying: false, minecraftJsonMessage: null as null | Record, showReconnect: false } export const appStatusState = proxy(initialState) export const resetAppStatusState = () => { Object.assign(appStatusState, initialState) } export const lastConnectOptions = { value: null as ConnectOptions | null } globalThis.lastConnectOptions = lastConnectOptions const saveReconnectOptions = (options: ConnectOptions) => { sessionStorage.setItem('reconnectOptions', JSON.stringify({ value: options, timestamp: Date.now() })) } export const reconnectReload = () => { if (lastConnectOptions.value) { saveReconnectOptions(lastConnectOptions.value) window.location.reload() } } export default () => { const lastState = useRef(JSON.parse(JSON.stringify(appStatusState))) const currentState = useSnapshot(appStatusState) const { active: replayActive } = useSnapshot(packetsRecordingState) const isOpen = useIsModalActive('app-status') if (isOpen) { lastState.current = JSON.parse(JSON.stringify(currentState)) } const usingState = (isOpen ? currentState : lastState.current) as typeof currentState const { isError, lastStatus, maybeRecoverable, status, hideDots, descriptionHint, loadingChunksData, loadingChunksDataPlayerChunk, minecraftJsonMessage, showReconnect } = usingState useDidUpdateEffect(() => { // todo play effect only when world successfully loaded if (!isOpen) { const startDiveAnimation = (divingElem: HTMLElement) => { divingElem.style.animationName = 'dive-animation' divingElem.parentElement!.style.perspective = '1200px' divingElem.onanimationend = () => { divingElem.parentElement!.style.perspective = '' divingElem.onanimationend = null } } const divingElem = document.querySelector('#viewer-canvas') let observer: MutationObserver | null = null if (divingElem) { startDiveAnimation(divingElem as HTMLElement) } else { observer = new MutationObserver((mutations) => { const divingElem = document.querySelector('#viewer-canvas') if (divingElem) { startDiveAnimation(divingElem as HTMLElement) observer!.disconnect() } }) observer.observe(document.body, { childList: true, subtree: true }) } return () => { if (observer) { observer.disconnect() } } } }, [isOpen]) const reconnect = () => { resetAppStatusState() window.dispatchEvent(new window.CustomEvent('connect', { detail: lastConnectOptions.value })) } useEffect(() => { const controller = new AbortController() window.addEventListener('keyup', (e) => { if ('input textarea select'.split(' ').includes((e.target as HTMLElement).tagName?.toLowerCase() ?? '')) return if (activeModalStack.at(-1)?.reactType !== 'app-status') return // todo do only if reconnect is possible if (e.code !== 'KeyR' || !lastConnectOptions.value) return reconnect() }, { signal: controller.signal }) return () => controller.abort() }, []) const displayAuthButton = status.includes('This server appears to be an online server and you are providing no authentication.') const hasVpnText = (text: string) => text.includes('VPN') || text.includes('Proxy') const displayVpnButton = hasVpnText(status) || (minecraftJsonMessage && hasVpnText(JSON.stringify(minecraftJsonMessage))) const authReconnectAction = async () => { let accounts = [] as AuthenticatedAccount[] updateAuthenticatedAccountData(oldAccounts => { accounts = oldAccounts return oldAccounts }) const account = await showOptionsModal('Choose account to connect with', [...accounts.map(account => account.username), 'Use other account']) if (!account) return lastConnectOptions.value!.authenticatedAccount = accounts.find(acc => acc.username === account) || true reconnect() } const lastAutoCapturedPackets = getLastAutoCapturedPackets() const lockConnect = appQueryParams.lockConnect === 'true' const wasDisconnected = showReconnect let backAction = undefined as (() => void) | undefined if (maybeRecoverable && (!lockConnect || !wasDisconnected)) { backAction = () => { if (!wasDisconnected) { hideModal(undefined, undefined, { force: true }) return } resetAppStatusState() miscUiState.gameLoaded = false miscUiState.loadedDataVersion = null window.loadedData = undefined if (activeModalStacks['main-menu']) { insertActiveModalStack('main-menu') if (activeModalStack.at(-1)?.reactType === 'app-status') { hideModal(undefined, undefined, { force: true }) // workaround: hide loader that was shown on world loading } } else { hideModal(undefined, undefined, { force: true }) } } } return { displayAuthButton ? '' : (isError ? guessProblem(status) : '') || descriptionHint }{ minecraftJsonMessage && }} backAction={backAction} actionsSlot={ <> {displayAuthButton &&