Show player count on landing page.

The server status needs to be stored in the redux store as it needs to be determined before
mounting the Landing component.
This commit is contained in:
Daniel Scalzi 2020-09-04 22:41:32 -04:00
parent cb68c34abe
commit 67e42ead78
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
11 changed files with 247 additions and 79 deletions

63
package-lock.json generated
View File

@ -1362,14 +1362,20 @@
} }
}, },
"@eslint/eslintrc": { "@eslint/eslintrc": {
"version": "0.1.0", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz",
"integrity": "sha512-bfL5365QSCmH6cPeFT7Ywclj8C7LiF7sO6mUGzZhtAMV7iID1Euq6740u/SRi4C80NOnVz/CEfK8/HO+nCAPJg==", "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "^6.12.4", "ajv": "^6.12.4",
"debug": "^4.1.1", "debug": "^4.1.1",
"espree": "^7.3.0",
"globals": "^12.1.0",
"ignore": "^4.0.6",
"import-fresh": "^3.2.1", "import-fresh": "^3.2.1",
"js-yaml": "^3.13.1",
"lodash": "^4.17.19",
"minimatch": "^3.0.4",
"strip-json-comments": "^3.1.1" "strip-json-comments": "^3.1.1"
}, },
"dependencies": { "dependencies": {
@ -1385,11 +1391,26 @@
"uri-js": "^4.2.2" "uri-js": "^4.2.2"
} }
}, },
"globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
"integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
"dev": true,
"requires": {
"type-fest": "^0.8.1"
}
},
"strip-json-comments": { "strip-json-comments": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true "dev": true
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
} }
} }
}, },
@ -1577,9 +1598,9 @@
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
"version": "12.12.54", "version": "12.12.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.54.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.55.tgz",
"integrity": "sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w==" "integrity": "sha512-Vd6xQUVvPCTm7Nx1N7XHcpX6t047ltm7TgcsOr4gFHjeYgwZevo+V7I1lfzHnj5BT5frztZ42+RTG4MwYw63dw=="
}, },
"@types/prop-types": { "@types/prop-types": {
"version": "15.7.3", "version": "15.7.3",
@ -4722,13 +4743,13 @@
"dev": true "dev": true
}, },
"eslint": { "eslint": {
"version": "7.8.0", "version": "7.8.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.1.tgz",
"integrity": "sha512-qgtVyLZqKd2ZXWnLQA4NtVbOyH56zivOAdBFWE54RFkSZjokzNrcP4Z0eVWsZ+84ByXv+jL9k/wE1ENYe8xRFw==", "integrity": "sha512-/2rX2pfhyUG0y+A123d0ccXtMm7DV7sH1m3lk9nk2DZ2LReq39FXHueR9xZwshE5MdfSf0xunSaMWRqyIA6M1w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/code-frame": "^7.0.0", "@babel/code-frame": "^7.0.0",
"@eslint/eslintrc": "^0.1.0", "@eslint/eslintrc": "^0.1.3",
"ajv": "^6.10.0", "ajv": "^6.10.0",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@ -6072,18 +6093,18 @@
} }
}, },
"got": { "got": {
"version": "11.5.2", "version": "11.6.0",
"resolved": "https://registry.npmjs.org/got/-/got-11.5.2.tgz", "resolved": "https://registry.npmjs.org/got/-/got-11.6.0.tgz",
"integrity": "sha512-yUhpEDLeuGiGJjRSzEq3kvt4zJtAcjKmhIiwNp/eUs75tRlXfWcHo5tcBaMQtnjHWC7nQYT5HkY/l0QOQTkVww==", "integrity": "sha512-ErhWb4IUjQzJ3vGs3+RR12NWlBDDkRciFpAkQ1LPUxi6OnwhGj07gQxjPsyIk69s7qMihwKrKquV6VQq7JNYLA==",
"requires": { "requires": {
"@sindresorhus/is": "^3.0.0", "@sindresorhus/is": "^3.1.1",
"@szmarczak/http-timer": "^4.0.5", "@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1", "@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0", "@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3", "cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1", "cacheable-request": "^7.0.1",
"decompress-response": "^6.0.0", "decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.0", "http2-wrapper": "^1.0.0-beta.5.2",
"lowercase-keys": "^2.0.0", "lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"responselike": "^2.0.0" "responselike": "^2.0.0"
@ -11440,9 +11461,9 @@
} }
}, },
"ts-node": { "ts-node": {
"version": "8.10.2", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz",
"integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==",
"dev": true, "dev": true,
"requires": { "requires": {
"arg": "^4.1.0", "arg": "^4.1.0",
@ -11564,9 +11585,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "3.9.7", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==",
"dev": true "dev": true
}, },
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {

View File

@ -35,7 +35,7 @@
"electron-updater": "^4.3.4", "electron-updater": "^4.3.4",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"github-syntax-dark": "^0.5.0", "github-syntax-dark": "^0.5.0",
"got": "^11.5.2", "got": "^11.6.0",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.27.0", "moment": "^2.27.0",
@ -58,7 +58,7 @@
"@types/jquery": "^3.5.1", "@types/jquery": "^3.5.1",
"@types/lodash": "^4.14.161", "@types/lodash": "^4.14.161",
"@types/mocha": "^8.0.3", "@types/mocha": "^8.0.3",
"@types/node": "^12.12.54", "@types/node": "^12.12.55",
"@types/react": "^16.9.49", "@types/react": "^16.9.49",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9", "@types/react-redux": "^7.1.9",
@ -77,7 +77,7 @@
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-ts": "^4.0.1", "electron-webpack-ts": "^4.0.1",
"eslint": "^7.8.0", "eslint": "^7.8.1",
"eslint-plugin-react": "^7.20.6", "eslint-plugin-react": "^7.20.6",
"helios-distribution-types": "1.0.0-pre.1", "helios-distribution-types": "1.0.0-pre.1",
"mocha": "^8.1.3", "mocha": "^8.1.3",
@ -89,9 +89,9 @@
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"redux": "^4.0.5", "redux": "^4.0.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^8.10.2", "ts-node": "^9.0.0",
"tsconfig-paths": "^3.9.0", "tsconfig-paths": "^3.9.0",
"typescript": "^3.9.7", "typescript": "^4.0.2",
"webpack": "^4.44.1" "webpack": "^4.44.1"
}, },
"repository": { "repository": {

View File

@ -53,13 +53,34 @@ export class HeliosDistribution {
export class HeliosServer { export class HeliosServer {
public readonly modules: HeliosModule[] public readonly modules: HeliosModule[]
public readonly hostname: string
public readonly port: number
constructor( constructor(
public readonly rawServer: Server public readonly rawServer: Server
) { ) {
const { hostname, port } = this.parseAddress()
this.hostname = hostname
this.port = port
this.modules = rawServer.modules.map(m => new HeliosModule(m, rawServer.id)) this.modules = rawServer.modules.map(m => new HeliosModule(m, rawServer.id))
} }
private parseAddress(): { hostname: string, port: number } {
// Srv record lookup here if needed.
if(this.rawServer.address.includes(':')) {
const pieces = this.rawServer.address.split(':')
const port = Number(pieces[1])
if(!Number.isInteger(port)) {
throw new Error(`Malformed server address for ${this.rawServer.id}. Port must be an integer!`)
}
return { hostname: pieces[0], port }
} else {
return { hostname: this.rawServer.address, port: 25565 }
}
}
} }
export class HeliosModule { export class HeliosModule {

View File

@ -35,17 +35,17 @@ export interface ServerStatus {
* Get the handshake packet. * Get the handshake packet.
* *
* @param protocol The client's protocol version. * @param protocol The client's protocol version.
* @param address The server address. * @param hostname The server hostname.
* @param port The server port. * @param port The server port.
* *
* @see https://wiki.vg/Server_List_Ping#Handshake * @see https://wiki.vg/Server_List_Ping#Handshake
*/ */
function getHandshakePacket(protocol: number, address: string, port: number): Buffer { function getHandshakePacket(protocol: number, hostname: string, port: number): Buffer {
return ServerBoundPacket.build() return ServerBoundPacket.build()
.writeVarInt(0x00) // Packet Id .writeVarInt(0x00) // Packet Id
.writeVarInt(protocol) .writeVarInt(protocol)
.writeString(address) .writeString(hostname)
.writeUnsignedShort(port) .writeUnsignedShort(port)
.writeVarInt(1) // State, 1 = status .writeVarInt(1) // State, 1 = status
.toBuffer() .toBuffer()
@ -80,19 +80,19 @@ function unifyStatusResponse(resp: ServerStatus): ServerStatus {
return resp return resp
} }
export function getServerStatus(protocol: number, address: string, port = 25565): Promise<ServerStatus | null> { export function getServerStatus(protocol: number, hostname: string, port = 25565): Promise<ServerStatus | undefined> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const socket = connect(port, address, () => { const socket = connect(port, hostname, () => {
socket.write(getHandshakePacket(protocol, address, port)) socket.write(getHandshakePacket(protocol, hostname, port))
socket.write(getRequestPacket()) socket.write(getRequestPacket())
}) })
socket.setTimeout(5000, () => { socket.setTimeout(5000, () => {
socket.destroy() socket.destroy()
logger.error(`Server Status Socket timed out (${address}:${port})`) logger.error(`Server Status Socket timed out (${hostname}:${port})`)
reject(new Error(`Server Status Socket timed out (${address}:${port})`)) reject(new Error(`Server Status Socket timed out (${hostname}:${port})`))
}) })
const maxTries = 2 const maxTries = 2
@ -122,7 +122,7 @@ export function getServerStatus(protocol: number, address: string, port = 25565)
if(iterations > maxTries) { if(iterations > maxTries) {
socket.destroy() socket.destroy()
reject(new Error(`Data read from ${address}:${port} exceeded ${maxTries} iterations, closing connection.`)) reject(new Error(`Data read from ${hostname}:${port} exceeded ${maxTries} iterations, closing connection.`))
return return
} }
++iterations ++iterations
@ -141,7 +141,7 @@ export function getServerStatus(protocol: number, address: string, port = 25565)
const result = inboundPacket.readString() const result = inboundPacket.readString()
try { try {
const parsed = JSON.parse(result) const parsed: ServerStatus = JSON.parse(result)
socket.end() socket.end()
resolve(unifyStatusResponse(parsed)) resolve(unifyStatusResponse(parsed))
} catch(err) { } catch(err) {
@ -164,17 +164,17 @@ export function getServerStatus(protocol: number, address: string, port = 25565)
if(err.code === 'ENOTFOUND') { if(err.code === 'ENOTFOUND') {
// ENOTFOUND = Unable to resolve. // ENOTFOUND = Unable to resolve.
logger.error(`Server ${address}:${port} not found!`) logger.error(`Server ${hostname}:${port} not found!`)
resolve(null) resolve(undefined)
return return
} else if(err.code === 'ECONNREFUSED') { } else if(err.code === 'ECONNREFUSED') {
// ECONNREFUSED = Unable to connect to port. // ECONNREFUSED = Unable to connect to port.
logger.error(`Server ${address}:${port} refused to connect, is the port correct?`) logger.error(`Server ${hostname}:${port} refused to connect, is the port correct?`)
resolve(null) resolve(undefined)
return return
} else { } else {
logger.error(`Error trying to pull server status (${address}:${port})`, err) logger.error(`Error trying to pull server status (${hostname}:${port})`, err)
resolve(null) resolve(undefined)
return return
} }
}) })

View File

@ -21,7 +21,7 @@ import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overl
import { LoggerUtil } from 'common/logging/loggerutil' import { LoggerUtil } from 'common/logging/loggerutil'
import { DistributionAPI } from 'common/distribution/DistributionAPI' import { DistributionAPI } from 'common/distribution/DistributionAPI'
import { getServerStatus } from 'common/mojang/net/ServerStatusAPI' import { getServerStatus, ServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { Distribution } from 'helios-distribution-types' import { Distribution } from 'helios-distribution-types'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory' import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
@ -40,6 +40,7 @@ interface ApplicationProps {
overlayQueue: OverlayPushAction<unknown>[] overlayQueue: OverlayPushAction<unknown>[]
distribution: HeliosDistribution distribution: HeliosDistribution
selectedServer: HeliosServer selectedServer: HeliosServer
selectedServerStatus: ServerStatus
} }
interface ApplicationState { interface ApplicationState {
@ -79,7 +80,7 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
} }
} }
getViewElement(): JSX.Element { private getViewElement = (): JSX.Element => {
// TODO debug remove // TODO debug remove
console.log('loading', this.props.currentView, this.state.workingView) console.log('loading', this.props.currentView, this.state.workingView)
switch(this.state.workingView) { switch(this.state.workingView) {
@ -89,7 +90,11 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
</> </>
case View.LANDING: case View.LANDING:
return <> return <>
<Landing distribution={this.props.distribution} selectedServer={this.props.selectedServer} /> <Landing
distribution={this.props.distribution}
selectedServer={this.props.selectedServer}
selectedServerStatus={this.props.selectedServerStatus}
/>
</> </>
case View.LOGIN: case View.LOGIN:
return <> return <>
@ -164,10 +169,19 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
return return
} else { } else {
const distro = new HeliosDistribution(rawDisto) const distro = new HeliosDistribution(rawDisto)
this.props.setDistribution(distro)
// TODO TEMP USE CONFIG // TODO TEMP USE CONFIG
// TODO TODO TODO TODO // TODO TODO TODO TODO
this.props.setSelectedServer(distro.servers[0]) const selectedServer: HeliosServer = distro.servers[0]
const { hostname, port } = selectedServer
let selectedServerStatus
try {
selectedServerStatus = await getServerStatus(47, hostname, port)
} catch(err) {
Application.logger.error('Failed to refresh server status', selectedServerStatus)
}
this.props.setDistribution(distro)
this.props.setSelectedServer(selectedServer)
this.props.setSelectedServerStatus(selectedServerStatus)
} }
// TODO Setup hook for distro refresh every ~ 5 mins. // TODO Setup hook for distro refresh every ~ 5 mins.
@ -186,16 +200,16 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
}) })
// TODO temp // TODO temp
setTimeout(() => { setTimeout(() => {
//this.props.setView(View.WELCOME) // this.props.setView(View.WELCOME)
this.props.pushGenericOverlay({ // this.props.pushGenericOverlay({
title: 'Load Distribution', // title: 'Load Distribution',
description: 'This is a test.', // description: 'This is a test.',
dismissible: false, // dismissible: false,
acknowledgeCallback: async () => { // acknowledgeCallback: async () => {
const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565) // const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565)
console.log(serverStatus) // console.log(serverStatus)
} // }
}) // })
// this.props.pushGenericOverlay({ // this.props.pushGenericOverlay({
// title: 'Test Title 2', // title: 'Test Title 2',
// description: 'Test Description', // description: 'Test Description',

View File

@ -1,3 +1,19 @@
.serverStatusWrapper-enter {
opacity: 0;
}
.serverStatusWrapper-enter-active {
opacity: 1;
transition: opacity 500ms, transform 500ms;
}
.serverStatusWrapper-exit {
opacity: 1;
}
.serverStatusWrapper-exit-active {
opacity: 0;
transition: opacity 500ms, transform 500ms;
}
/******************************************************************************* /*******************************************************************************
* * * *
* Landing View (Structural Styles) * * Landing View (Structural Styles) *

View File

@ -1,10 +1,12 @@
import * as React from 'react' import * as React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { CSSTransition } from 'react-transition-group'
import { StoreType } from '../../redux/store' import { StoreType } from '../../redux/store'
import { AppActionDispatch } from '../..//redux/actions/appActions' import { AppActionDispatch } from '../..//redux/actions/appActions'
import { OverlayActionDispatch } from '../../redux/actions/overlayActions' import { OverlayActionDispatch } from '../../redux/actions/overlayActions'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory' import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
import { ServerStatus, getServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus' import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse' import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI' import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI'
@ -18,16 +20,19 @@ import './Landing.css'
interface LandingProps { interface LandingProps {
distribution: HeliosDistribution distribution: HeliosDistribution
selectedServer: HeliosServer selectedServer: HeliosServer
selectedServerStatus: ServerStatus
} }
interface LandingState { interface LandingState {
mojangStatuses: MojangStatus[] mojangStatuses: MojangStatus[]
outdatedServerStatus: boolean
} }
const mapState = (state: StoreType): Partial<LandingProps> => { const mapState = (state: StoreType): Partial<LandingProps> => {
return { return {
distribution: state.app.distribution!, distribution: state.app.distribution!,
selectedServer: state.app.selectedServer! selectedServer: state.app.selectedServer!,
selectedServerStatus: state.app.selectedServerStatus!
} }
} }
const mapDispatch = { const mapDispatch = {
@ -42,11 +47,13 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
private static readonly logger = LoggerUtil.getLogger('LandingTSX') private static readonly logger = LoggerUtil.getLogger('LandingTSX')
private mojangStatusInterval!: NodeJS.Timeout private mojangStatusInterval!: NodeJS.Timeout
private serverStatusInterval!: NodeJS.Timeout
constructor(props: InternalLandingProps) { constructor(props: InternalLandingProps) {
super(props) super(props)
this.state = { this.state = {
mojangStatuses: [] mojangStatuses: [],
outdatedServerStatus: false
} }
} }
@ -60,12 +67,21 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
await this.loadMojangStatuses() await this.loadMojangStatuses()
}, 300000) }, 300000)
this.serverStatusInterval = setInterval(async () => {
Landing.logger.info('Refreshing selected server status..')
this.setState({
...this.state,
outdatedServerStatus: true
})
}, 300000)
} }
componentWillUnmount(): void { componentWillUnmount(): void {
// Clean up intervals. // Clean up intervals.
clearInterval(this.mojangStatusInterval) clearInterval(this.mojangStatusInterval)
clearInterval(this.serverStatusInterval)
} }
@ -92,6 +108,34 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
} }
private syncServerStatus = async (): Promise<void> => {
let serverStatus: ServerStatus | undefined
if(this.props.selectedServer != null) {
const { hostname, port } = this.props.selectedServer
try {
serverStatus = await getServerStatus(
47,
hostname,
port
)
} catch(err) {
Landing.logger.error('Error while refreshing server status', err)
}
} else {
serverStatus = undefined
}
this.props.setSelectedServerStatus(serverStatus)
}
private finishServerSync = async (): Promise<void> => {
this.setState({
...this.state,
outdatedServerStatus: false
})
}
private getMainMojangStatusColor = (): string => { private getMainMojangStatusColor = (): string => {
const essential = this.state.mojangStatuses.filter(s => s.essential) const essential = this.state.mojangStatuses.filter(s => s.essential)
@ -137,9 +181,14 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
this.props.pushServerSelectOverlay({ this.props.pushServerSelectOverlay({
servers: this.props.distribution.servers, servers: this.props.distribution.servers,
selectedId: this.props.selectedServer.rawServer.id, selectedId: this.props.selectedServer.rawServer.id,
onSelection: (serverId: string) => { onSelection: async (serverId: string) => {
Landing.logger.info('Server Selection Change:', serverId) Landing.logger.info('Server Selection Change:', serverId)
this.props.setSelectedServer(this.props.distribution.getServerById(serverId)!) const next: HeliosServer = this.props.distribution.getServerById(serverId)!
this.props.setSelectedServer(next)
this.setState({
...this.state,
outdatedServerStatus: true
})
} }
}) })
} }
@ -152,6 +201,19 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
} }
} }
private getSelectedServerStatusText = (): string => {
return this.props.selectedServerStatus != null ? 'PLAYERS' : 'SERVER'
}
private getSelectedServerCount = (): string => {
if(this.props.selectedServerStatus != null) {
const { online, max } = this.props.selectedServerStatus.players
return `${online}/${max}`
} else {
return 'OFFLINE'
}
}
render(): JSX.Element { render(): JSX.Element {
return <> return <>
@ -252,10 +314,21 @@ class Landing extends React.Component<InternalLandingProps, LandingState> {
<div id="left"> <div id="left">
<div className="bot_wrapper"> <div className="bot_wrapper">
<div id="content"> <div id="content">
<div id="server_status_wrapper">
<span className="bot_label" id="landingPlayerLabel">SERVER</span> <CSSTransition
<span id="player_count">OFFLINE</span> in={!this.state.outdatedServerStatus}
</div> timeout={500}
classNames="serverStatusWrapper"
unmountOnExit
onEnter={this.syncServerStatus}
onExited={this.finishServerSync}
>
<div id="server_status_wrapper">
<span className="bot_label" id="landingPlayerLabel">{this.getSelectedServerStatusText()}</span>
<span id="player_count">{this.getSelectedServerCount()}</span>
</div>
</CSSTransition>
<div className="bot_divider"></div> <div className="bot_divider"></div>
<div id="mojangStatusWrapper"> <div id="mojangStatusWrapper">
<span className="bot_label">MOJANG STATUS</span> <span className="bot_label">MOJANG STATUS</span>

View File

@ -10,7 +10,7 @@ import '../shared-select/SharedSelect.css'
export interface ServerSelectOverlayProps { export interface ServerSelectOverlayProps {
servers: HeliosServer[] servers: HeliosServer[]
selectedId: string selectedId: string
onSelection: (serverId: string) => void onSelection: (serverId: string) => Promise<void>
} }
interface ServerSelectOverlayState { interface ServerSelectOverlayState {
@ -36,7 +36,7 @@ class ServerSelectOverlay extends React.Component<InternalServerSelectOverlayPro
private onSelectClick = async (): Promise<void> => { private onSelectClick = async (): Promise<void> => {
try { try {
this.props.onSelection(this.state.selectedId) await this.props.onSelection(this.state.selectedId)
} catch(err) { } catch(err) {
this.logger.error('Uncaught error in server select confirmation.', err) this.logger.error('Uncaught error in server select confirmation.', err)
} }

View File

@ -30,6 +30,7 @@ ReactDOM.render(
overlayQueue={store.getState().overlayQueue} overlayQueue={store.getState().overlayQueue}
distribution={store.getState().app.distribution!} distribution={store.getState().app.distribution!}
selectedServer={store.getState().app.selectedServer!} selectedServer={store.getState().app.selectedServer!}
selectedServerStatus={store.getState().app.selectedServerStatus!}
/> />
</Provider> </Provider>
</AppContainer>, </AppContainer>,

View File

@ -1,37 +1,51 @@
import { Action } from 'redux' import { Action } from 'redux'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory' import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
export enum AppActionType { export enum AppActionType {
SetDistribution = 'SET_DISTRIBUTION', SetDistribution = 'SET_DISTRIBUTION',
SetSelectedServer = 'SET_SELECTED_SERVER' SetSelectedServer = 'SET_SELECTED_SERVER',
SetSelectedServerStatus = 'SET_SELECTED_SERVER_STATUS'
} }
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AppAction extends Action {} export interface AppAction extends Action {}
export interface SetDistributionAction extends AppAction { export interface SetDistributionAction extends AppAction {
payload: HeliosDistribution payload?: HeliosDistribution
} }
export interface SetSelectedServerAction extends AppAction { export interface SetSelectedServerAction extends AppAction {
payload: HeliosServer payload?: HeliosServer
} }
export function setDistribution(distribution: HeliosDistribution): SetDistributionAction { export interface SetSelectedServerStatusAction extends AppAction {
payload?: ServerStatus
}
export function setDistribution(distribution?: HeliosDistribution): SetDistributionAction {
return { return {
type: AppActionType.SetDistribution, type: AppActionType.SetDistribution,
payload: distribution payload: distribution
} }
} }
export function setSelectedServer(server: HeliosServer): SetSelectedServerAction { export function setSelectedServer(server?: HeliosServer): SetSelectedServerAction {
return { return {
type: AppActionType.SetSelectedServer, type: AppActionType.SetSelectedServer,
payload: server payload: server
} }
} }
export function setSelectedServerStatus(serverStatus?: ServerStatus): SetSelectedServerStatusAction {
return {
type: AppActionType.SetSelectedServerStatus,
payload: serverStatus
}
}
export const AppActionDispatch = { export const AppActionDispatch = {
setDistribution: (d: HeliosDistribution): SetDistributionAction => setDistribution(d), setDistribution: (d?: HeliosDistribution): SetDistributionAction => setDistribution(d),
setSelectedServer: (s: HeliosServer): SetSelectedServerAction => setSelectedServer(s) setSelectedServer: (s?: HeliosServer): SetSelectedServerAction => setSelectedServer(s),
setSelectedServerStatus: (ss?: ServerStatus): SetSelectedServerStatusAction => setSelectedServerStatus(ss)
} }

View File

@ -1,15 +1,18 @@
import { AppActionType, AppAction, SetDistributionAction, SetSelectedServerAction } from '../actions/appActions' import { AppActionType, AppAction, SetDistributionAction, SetSelectedServerAction, SetSelectedServerStatusAction } from '../actions/appActions'
import { Reducer } from 'redux' import { Reducer } from 'redux'
import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory' import { HeliosDistribution, HeliosServer } from 'common/distribution/DistributionFactory'
import { ServerStatus } from 'common/mojang/net/ServerStatusAPI'
export interface AppState { export interface AppState {
distribution: HeliosDistribution | null distribution?: HeliosDistribution
selectedServer: HeliosServer | null selectedServer?: HeliosServer
selectedServerStatus?: ServerStatus
} }
const defaultAppState: AppState = { const defaultAppState: AppState = {
distribution: null, distribution: undefined,
selectedServer: null selectedServer: undefined,
selectedServerStatus: undefined
} }
const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => { const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => {
@ -24,6 +27,11 @@ const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, actio
...state, ...state,
selectedServer: (action as SetSelectedServerAction).payload selectedServer: (action as SetSelectedServerAction).payload
} }
case AppActionType.SetSelectedServerStatus:
return {
...state,
selectedServerStatus: (action as SetSelectedServerStatusAction).payload
}
} }
return state return state
} }