153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import fs from 'fs'
|
|
import { Transition } from 'react-transition-group'
|
|
import { proxy, subscribe, useSnapshot } from 'valtio'
|
|
import { useEffect, useState } from 'react'
|
|
import { activeModalStack, miscUiState, openOptionsMenu, showModal } from '../globalState'
|
|
import { openGithub } from '../utils'
|
|
import { setLoadingScreenStatus } from '../appStatus'
|
|
import { openFilePicker, copyFilesAsync, mkdirRecursive, openWorldDirectory, removeFileRecursiveAsync } from '../browserfs'
|
|
|
|
import MainMenu from './MainMenu'
|
|
|
|
const isMainMenu = () => {
|
|
return activeModalStack.length === 0 && !miscUiState.gameLoaded
|
|
}
|
|
|
|
const refreshApp = async (failedUpdate = false) => {
|
|
try {
|
|
const registration = await navigator.serviceWorker.getRegistration()
|
|
if (registration) {
|
|
// First, disconnect all clients
|
|
const clients = await window.clients?.matchAll() || []
|
|
await Promise.all(clients.map(client => client.postMessage('SKIP_WAITING')))
|
|
|
|
// Force the waiting service worker to become active
|
|
if (registration.waiting) {
|
|
registration.waiting.postMessage('SKIP_WAITING')
|
|
}
|
|
|
|
// Add timeout to prevent infinite waiting
|
|
const unregisterPromise = registration.unregister()
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
setTimeout(() => reject(new Error('SW unregister timeout')), 3000)
|
|
})
|
|
|
|
await Promise.race([unregisterPromise, timeoutPromise])
|
|
.catch(err => {
|
|
console.warn('SW unregister error:', err)
|
|
if (isMainMenu()) {
|
|
alert('Failed to unregister SW: ' + err)
|
|
}
|
|
})
|
|
}
|
|
|
|
if (failedUpdate) {
|
|
await new Promise(resolve => { setTimeout(resolve, 2000) })
|
|
}
|
|
|
|
if (!isMainMenu()) return
|
|
|
|
if (failedUpdate) {
|
|
sessionStorage.justReloaded = false
|
|
// try to force bypass cache
|
|
location.search = '?update=true'
|
|
} else {
|
|
window.justReloaded = true
|
|
sessionStorage.justReloaded = true
|
|
window.location.reload()
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to refresh app:', err)
|
|
if (!isMainMenu()) {
|
|
alert('Critical error on refreshApp: ' + err)
|
|
// Fallback to force reload if something goes wrong
|
|
window.location.reload()
|
|
}
|
|
}
|
|
}
|
|
|
|
export const mainMenuState = proxy({
|
|
serviceWorkerLoaded: false,
|
|
})
|
|
|
|
// todo clean
|
|
let disableAnimation = false
|
|
export default () => {
|
|
const haveModals = useSnapshot(activeModalStack).length
|
|
const { gameLoaded, fsReady, appConfig, singleplayerAvailable } = useSnapshot(miscUiState)
|
|
|
|
const noDisplay = haveModals || gameLoaded || !fsReady
|
|
|
|
useEffect(() => {
|
|
if (noDisplay && fsReady) disableAnimation = true
|
|
}, [noDisplay])
|
|
|
|
const [versionStatus, setVersionStatus] = useState('')
|
|
const [versionTitle, setVersionTitle] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (process.env.SINGLE_FILE_BUILD_MODE) {
|
|
setVersionStatus('(single file build)')
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
setVersionStatus('(dev)')
|
|
} else {
|
|
fetch('./version.txt').then(async (f) => {
|
|
if (f.status === 404) return
|
|
const contents = await f.text()
|
|
const isLatest = contents === process.env.BUILD_VERSION
|
|
if (!isLatest && sessionStorage.justReloaded) {
|
|
setVersionStatus('(force reloading, wait)')
|
|
void refreshApp(true)
|
|
return
|
|
}
|
|
const upStatus = () => {
|
|
setVersionStatus(`(${isLatest ? 'latest' : 'new version available'}${mainMenuState.serviceWorkerLoaded ? ', Downloaded' : ''})`)
|
|
}
|
|
subscribe(mainMenuState, upStatus)
|
|
upStatus()
|
|
setVersionTitle(`Loaded: ${process.env.BUILD_VERSION}. Remote: ${contents}`)
|
|
}, () => {
|
|
setVersionStatus('(offline)')
|
|
})
|
|
}
|
|
}, [])
|
|
|
|
const mapsProviderUrl = appConfig?.mapsProvider && new URL(appConfig?.mapsProvider)
|
|
if (mapsProviderUrl && location.origin !== 'https://mcraft.fun') {
|
|
mapsProviderUrl.searchParams.set('to', location.href)
|
|
}
|
|
|
|
// todo clean, use custom csstransition
|
|
return <Transition in={!noDisplay} timeout={disableAnimation ? 0 : 100} mountOnEnter unmountOnExit>
|
|
{(state) => <div style={{ transition: state === 'exiting' || disableAnimation ? '' : '100ms opacity ease-in', ...state === 'entered' ? { opacity: 1 } : { opacity: 0 } }}>
|
|
<MainMenu
|
|
singleplayerAvailable={singleplayerAvailable}
|
|
connectToServerAction={() => showModal({ reactType: 'serversList' })}
|
|
singleplayerAction={async () => {
|
|
showModal({ reactType: 'singleplayer' })
|
|
}}
|
|
githubAction={() => openGithub()}
|
|
optionsAction={() => openOptionsMenu('main')}
|
|
bottomRightLinks={process.env.MAIN_MENU_LINKS}
|
|
openFileAction={e => {
|
|
if (!!window.showDirectoryPicker && !e.shiftKey) {
|
|
void openWorldDirectory()
|
|
} else {
|
|
openFilePicker()
|
|
}
|
|
}}
|
|
mapsProvider={mapsProviderUrl?.toString()}
|
|
versionStatus={versionStatus}
|
|
versionTitle={versionTitle}
|
|
onVersionStatusClick={async () => {
|
|
setVersionStatus('(reloading)')
|
|
await refreshApp()
|
|
}}
|
|
onVersionTextClick={async () => {
|
|
openGithub(process.env.RELEASE_LINK)
|
|
}}
|
|
versionText={process.env.RELEASE_TAG}
|
|
/>
|
|
</div>}
|
|
</Transition>
|
|
}
|