238 lines
8.8 KiB
TypeScript
238 lines
8.8 KiB
TypeScript
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<string, string>,
|
|
loadingChunksDataPlayerChunk: null as null | { x: number, z: number },
|
|
isDisplaying: false,
|
|
minecraftJsonMessage: null as null | Record<string, any>,
|
|
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.')
|
|
|| JSON.stringify(minecraftJsonMessage ?? {}).toLowerCase().includes('authenticate')
|
|
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 <DiveTransition open={isOpen} isError={isError}>
|
|
<AppStatus
|
|
status={status}
|
|
isError={isError || status === ''} // display back button if status is empty as probably our app is errored
|
|
hideDots={hideDots}
|
|
lastStatus={lastStatus}
|
|
showReconnect={showReconnect}
|
|
onReconnect={reconnectReload}
|
|
description={<>{
|
|
displayAuthButton ? '' : (isError ? guessProblem(status) : '') || descriptionHint
|
|
}{
|
|
minecraftJsonMessage && <MessageFormattedString message={minecraftJsonMessage} />
|
|
}</>}
|
|
backAction={backAction}
|
|
actionsSlot={
|
|
<>
|
|
{displayAuthButton && <Button label='Authenticate' onClick={authReconnectAction} />}
|
|
{displayVpnButton && <PossiblyVpnBypassProxyButton reconnect={reconnect} />}
|
|
{replayActive && <Button label={`Download Packets Replay ${replayLogger?.contents.split('\n').length}L`} onClick={downloadPacketsReplay} />}
|
|
{wasDisconnected && lastAutoCapturedPackets && <Button label={`Inspect Last ${lastAutoCapturedPackets} Packets`} onClick={() => downloadAutoCapturedPackets()} />}
|
|
</>
|
|
}
|
|
>
|
|
{loadingChunksData && <LoadingChunks regionFiles={Object.keys(loadingChunksData)} stateMap={loadingChunksData} playerChunk={loadingChunksDataPlayerChunk} />}
|
|
{isOpen && <DisplayingIndicator />}
|
|
</AppStatus>
|
|
</DiveTransition>
|
|
}
|
|
|
|
const DisplayingIndicator = () => {
|
|
useEffect(() => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
appStatusState.isDisplaying = true
|
|
})
|
|
})
|
|
}, [])
|
|
|
|
return <div />
|
|
}
|
|
|
|
const PossiblyVpnBypassProxyButton = ({ reconnect }: { reconnect: () => void }) => {
|
|
const [vpnBypassProxy, setVpnBypassProxy] = useState('')
|
|
|
|
const useVpnBypassProxyAction = () => {
|
|
updateLoadedServerData((data) => {
|
|
data.proxyOverride = vpnBypassProxy
|
|
return data
|
|
}, lastConnectOptions.value?.serverIndex)
|
|
lastConnectOptions.value!.proxy = vpnBypassProxy
|
|
reconnect()
|
|
}
|
|
|
|
useEffect(() => {
|
|
const proxy = lastConnectOptions.value?.proxy
|
|
if (!proxy) return
|
|
getProxyDetails(proxy)
|
|
.then(async (r) => r.json())
|
|
.then(({ capabilities }) => {
|
|
const { vpnBypassProxy } = capabilities
|
|
if (!vpnBypassProxy) return
|
|
setVpnBypassProxy(vpnBypassProxy)
|
|
})
|
|
.catch(() => { })
|
|
}, [])
|
|
|
|
if (!vpnBypassProxy) return
|
|
return <Button label='Use VPN bypass proxy' onClick={useVpnBypassProxyAction} />
|
|
}
|