Application.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. import { hot } from 'react-hot-loader/root'
  2. import * as React from 'react'
  3. import Frame from './frame/Frame'
  4. import Welcome from './welcome/Welcome'
  5. import { connect } from 'react-redux'
  6. import { View } from '../meta/Views'
  7. import Landing from './landing/Landing'
  8. import Login from './login/Login'
  9. import Loader from './loader/Loader'
  10. import Settings from './settings/Settings'
  11. import Overlay from './overlay/Overlay'
  12. import Fatal from './fatal/Fatal'
  13. import { StoreType } from '../redux/store'
  14. import { CSSTransition } from 'react-transition-group'
  15. import { ViewActionDispatch } from '../redux/actions/viewActions'
  16. import { throttle } from 'lodash'
  17. import { readdir } from 'fs-extra'
  18. import { join } from 'path'
  19. import { AppActionDispatch } from '../redux/actions/appActions'
  20. import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions'
  21. import { LoggerUtil } from 'common/logging/loggerutil'
  22. import { DistributionAPI } from 'common/distribution/DistributionAPI'
  23. import { getServerStatus, ServerStatus } from 'common/mojang/net/ServerStatusAPI'
  24. import { Distribution } from 'helios-distribution-types'
  25. import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
  26. import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
  27. import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
  28. import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
  29. import { RestResponseStatus } from 'common/got/RestResponse'
  30. import './Application.css'
  31. declare const __static: string
  32. function setBackground(id: number) {
  33. import(`../../../static/images/backgrounds/${id}.jpg`).then(mdl => {
  34. document.body.style.backgroundImage = `url('${mdl.default}')`
  35. })
  36. }
  37. interface ApplicationProps {
  38. currentView: View
  39. overlayQueue: OverlayPushAction<unknown>[]
  40. distribution: HeliosDistribution
  41. selectedServer?: HeliosServer
  42. selectedServerStatus?: ServerStatus
  43. mojangStatuses: MojangStatus[]
  44. }
  45. interface ApplicationState {
  46. loading: boolean
  47. showMain: boolean
  48. renderMain: boolean
  49. workingView: View
  50. }
  51. const mapState = (state: StoreType): Partial<ApplicationProps> => {
  52. return {
  53. currentView: state.currentView,
  54. overlayQueue: state.overlayQueue,
  55. distribution: state.app.distribution,
  56. selectedServer: state.app.selectedServer,
  57. mojangStatuses: state.app.mojangStatuses
  58. }
  59. }
  60. const mapDispatch = {
  61. ...AppActionDispatch,
  62. ...ViewActionDispatch,
  63. ...OverlayActionDispatch
  64. }
  65. type InternalApplicationProps = ApplicationProps & typeof mapDispatch
  66. class Application extends React.Component<InternalApplicationProps, ApplicationState> {
  67. private static readonly logger = LoggerUtil.getLogger('Application')
  68. private mojangStatusInterval!: NodeJS.Timeout
  69. private serverStatusInterval!: NodeJS.Timeout
  70. private bkid!: number
  71. constructor(props: InternalApplicationProps) {
  72. super(props)
  73. this.state = {
  74. loading: true,
  75. showMain: false,
  76. renderMain: false,
  77. workingView: props.currentView
  78. }
  79. }
  80. async componentDidMount(): Promise<void> {
  81. this.mojangStatusInterval = setInterval(async () => {
  82. Application.logger.info('Refreshing Mojang Statuses..')
  83. await this.loadMojangStatuses()
  84. }, 300000)
  85. this.serverStatusInterval = setInterval(async () => {
  86. Application.logger.info('Refreshing selected server status..')
  87. await this.syncServerStatus()
  88. }, 300000)
  89. }
  90. componentWillUnmount(): void {
  91. // Clean up intervals.
  92. clearInterval(this.mojangStatusInterval)
  93. clearInterval(this.serverStatusInterval)
  94. }
  95. async componentDidUpdate(prevProps: InternalApplicationProps): Promise<void> {
  96. if(this.props.selectedServer?.rawServer.id !== prevProps.selectedServer?.rawServer.id) {
  97. await this.syncServerStatus()
  98. }
  99. }
  100. /**
  101. * Load the mojang statuses and add them to the global store.
  102. */
  103. private loadMojangStatuses = async (): Promise<void> => {
  104. const response: MojangResponse<MojangStatus[]> = await MojangRestAPI.status()
  105. if(response.responseStatus !== RestResponseStatus.SUCCESS) {
  106. Application.logger.warn('Failed to retrieve Mojang Statuses.')
  107. }
  108. // TODO Temp workaround because their status checker always shows
  109. // this as red. https://bugs.mojang.com/browse/WEB-2303
  110. const statuses = response.data
  111. for(const status of statuses) {
  112. if(status.service === 'sessionserver.mojang.com' || status.service === 'minecraft.net') {
  113. status.status = MojangStatusColor.GREEN
  114. }
  115. }
  116. this.props.setMojangStatuses(response.data)
  117. }
  118. /**
  119. * Fetch the status of the selected server and store it in the global store.
  120. */
  121. private syncServerStatus = async (): Promise<void> => {
  122. let serverStatus: ServerStatus | undefined
  123. if(this.props.selectedServer != null) {
  124. const { hostname, port } = this.props.selectedServer
  125. try {
  126. serverStatus = await getServerStatus(
  127. 47,
  128. hostname,
  129. port
  130. )
  131. } catch(err) {
  132. Application.logger.error('Error while refreshing server status', err)
  133. }
  134. } else {
  135. serverStatus = undefined
  136. }
  137. this.props.setSelectedServerStatus(serverStatus)
  138. }
  139. private getViewElement = (): JSX.Element => {
  140. // TODO debug remove
  141. console.log('loading', this.props.currentView, this.state.workingView)
  142. switch(this.state.workingView) {
  143. case View.WELCOME:
  144. return <>
  145. <Welcome />
  146. </>
  147. case View.LANDING:
  148. return <>
  149. <Landing
  150. distribution={this.props.distribution}
  151. selectedServer={this.props.selectedServer}
  152. selectedServerStatus={this.props.selectedServerStatus}
  153. mojangStatuses={this.props.mojangStatuses}
  154. />
  155. </>
  156. case View.LOGIN:
  157. return <>
  158. <Login cancelable={false} />
  159. </>
  160. case View.SETTINGS:
  161. return <>
  162. <Settings />
  163. </>
  164. case View.FATAL:
  165. return <>
  166. <Fatal />
  167. </>
  168. case View.NONE:
  169. return <></>
  170. }
  171. }
  172. private hasOverlay = (): boolean => {
  173. return this.props.overlayQueue.length > 0
  174. }
  175. private updateWorkingView = throttle(() => {
  176. // TODO debug remove
  177. console.log('Setting to', this.props.currentView)
  178. this.setState({
  179. ...this.state,
  180. workingView: this.props.currentView
  181. })
  182. }, 200)
  183. private finishLoad = (): void => {
  184. if(this.props.currentView !== View.FATAL) {
  185. setBackground(this.bkid)
  186. }
  187. this.showMain()
  188. }
  189. private showMain = (): void => {
  190. this.setState({
  191. ...this.state,
  192. showMain: true
  193. })
  194. }
  195. private initLoad = async (): Promise<void> => {
  196. if(this.state.loading) {
  197. const MIN_LOAD = 800
  198. const start = Date.now()
  199. // Initial distribution load.
  200. const distroAPI = new DistributionAPI('C:\\Users\\user\\AppData\\Roaming\\Helios Launcher')
  201. let rawDisto: Distribution
  202. try {
  203. rawDisto = await distroAPI.testLoad()
  204. console.log('distro', distroAPI)
  205. } catch(err) {
  206. console.log('EXCEPTION IN DISTRO LOAD TODO TODO TODO', err)
  207. rawDisto = null!
  208. }
  209. // Fatal error
  210. if(rawDisto == null) {
  211. this.props.setView(View.FATAL)
  212. this.setState({
  213. ...this.state,
  214. loading: false,
  215. workingView: View.FATAL
  216. })
  217. return
  218. } else {
  219. // For debugging display.
  220. // for(let i=0; i<10; i++) {
  221. // rawDisto.servers.push(rawDisto.servers[1])
  222. // }
  223. const distro = new HeliosDistribution(rawDisto)
  224. // TODO TEMP USE CONFIG
  225. // TODO TODO TODO TODO
  226. const selectedServer: HeliosServer = distro.servers[0]
  227. const { hostname, port } = selectedServer
  228. let selectedServerStatus
  229. try {
  230. selectedServerStatus = await getServerStatus(47, hostname, port)
  231. } catch(err) {
  232. Application.logger.error('Failed to refresh server status', selectedServerStatus)
  233. }
  234. this.props.setDistribution(distro)
  235. this.props.setSelectedServer(selectedServer)
  236. this.props.setSelectedServerStatus(selectedServerStatus)
  237. }
  238. // Load initial mojang statuses.
  239. Application.logger.info('Loading mojang statuses..')
  240. await this.loadMojangStatuses()
  241. // TODO Setup hook for distro refresh every ~ 5 mins.
  242. // Pick a background id.
  243. this.bkid = Math.floor((Math.random() * (await readdir(join(__static, 'images', 'backgrounds'))).length))
  244. this.bkid = 3 // TEMP
  245. const endLoad = () => {
  246. // TODO determine correct view
  247. // either welcome, landing, or login
  248. this.props.setView(View.LANDING)
  249. this.setState({
  250. ...this.state,
  251. loading: false,
  252. workingView: View.LANDING
  253. })
  254. // TODO temp
  255. setTimeout(() => {
  256. // this.props.setView(View.WELCOME)
  257. // this.props.pushGenericOverlay({
  258. // title: 'Load Distribution',
  259. // description: 'This is a test.',
  260. // dismissible: false,
  261. // acknowledgeCallback: async () => {
  262. // const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565)
  263. // console.log(serverStatus)
  264. // }
  265. // })
  266. // this.props.pushGenericOverlay({
  267. // title: 'Test Title 2',
  268. // description: 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.',
  269. // dismissible: true
  270. // })
  271. // this.props.pushGenericOverlay({
  272. // title: 'Test Title IMPORTANT',
  273. // description: 'Test Description',
  274. // dismissible: true
  275. // }, true)
  276. }, 5000)
  277. }
  278. const diff = Date.now() - start
  279. if(diff < MIN_LOAD) {
  280. setTimeout(endLoad, MIN_LOAD-diff)
  281. } else {
  282. endLoad()
  283. }
  284. }
  285. }
  286. render(): JSX.Element {
  287. return (
  288. <>
  289. <Frame />
  290. <CSSTransition
  291. in={this.state.showMain}
  292. appear={true}
  293. timeout={500}
  294. classNames="appWrapper"
  295. unmountOnExit
  296. >
  297. <div className="appWrapper" {...(this.hasOverlay() ? {overlay: 'true'} : {})}>
  298. <CSSTransition
  299. in={this.props.currentView == this.state.workingView}
  300. appear={true}
  301. timeout={500}
  302. classNames="appWrapper"
  303. unmountOnExit
  304. onExited={this.updateWorkingView}
  305. >
  306. {this.getViewElement()}
  307. </CSSTransition>
  308. </div>
  309. </CSSTransition>
  310. <CSSTransition
  311. in={this.hasOverlay()}
  312. appear={true}
  313. timeout={500}
  314. classNames="appWrapper"
  315. unmountOnExit
  316. >
  317. <Overlay overlayQueue={this.props.overlayQueue} />
  318. </CSSTransition>
  319. <CSSTransition
  320. in={this.state.loading}
  321. appear={true}
  322. timeout={300}
  323. classNames="loader"
  324. unmountOnExit
  325. onEnter={this.initLoad}
  326. onExited={this.finishLoad}
  327. >
  328. <Loader />
  329. </CSSTransition>
  330. </>
  331. )
  332. }
  333. }
  334. export default hot(connect<unknown, typeof mapDispatch>(mapState, mapDispatch)(Application))