344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { proxy, useSnapshot } from 'valtio'
|
|
import { qsOptions } from '../optionsStorage'
|
|
import { ConnectOptions } from '../connect'
|
|
import { hideCurrentModal, miscUiState, showModal } from '../globalState'
|
|
import ServersList from './ServersList'
|
|
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
|
import { useDidUpdateEffect } from './utils'
|
|
import { useIsModalActive } from './utilsApp'
|
|
|
|
interface StoreServerItem extends BaseServerInfo {
|
|
lastJoined?: number
|
|
description?: string
|
|
optionsOverride?: Record<string, any>
|
|
autoLogin?: Record<string, string>
|
|
}
|
|
|
|
type ServerResponse = {
|
|
online: boolean
|
|
version?: {
|
|
name_raw: string
|
|
}
|
|
// display tooltip
|
|
players?: {
|
|
online: number
|
|
max: number
|
|
list: Array<{
|
|
name_raw: string
|
|
name_clean: string
|
|
}>
|
|
}
|
|
icon?: string
|
|
motd?: {
|
|
raw: string
|
|
}
|
|
// todo circle error icon
|
|
mods?: Array<{ name, version }>
|
|
// todo display via hammer icon
|
|
software?: string
|
|
plugins?: Array<{ name, version }>
|
|
}
|
|
|
|
type AdditionalDisplayData = {
|
|
formattedText: string
|
|
textNameRight: string
|
|
icon?: string
|
|
}
|
|
|
|
const getInitialServersList = () => {
|
|
if (localStorage['serversList']) return JSON.parse(localStorage['serversList']) as StoreServerItem[]
|
|
|
|
const servers = [] as StoreServerItem[]
|
|
|
|
const legacyServersList = localStorage['serverHistory'] ? JSON.parse(localStorage['serverHistory']) as string[] : null
|
|
if (legacyServersList) {
|
|
for (const server of legacyServersList) {
|
|
if (!server || localStorage['server'] === server) continue
|
|
servers.push({ ip: server, lastJoined: Date.now() })
|
|
}
|
|
}
|
|
|
|
if (localStorage['server']) {
|
|
const legacyLastJoinedServer: StoreServerItem = {
|
|
ip: localStorage['server'],
|
|
passwordOverride: localStorage['password'],
|
|
versionOverride: localStorage['version'],
|
|
lastJoined: Date.now()
|
|
}
|
|
servers.push(legacyLastJoinedServer)
|
|
}
|
|
|
|
if (servers.length === 0) { // server list is empty, let's suggest some
|
|
for (const server of miscUiState.appConfig?.promoteServers ?? []) {
|
|
servers.push({
|
|
ip: server.ip,
|
|
description: server.description,
|
|
versionOverride: server.version,
|
|
})
|
|
}
|
|
}
|
|
|
|
return servers
|
|
}
|
|
|
|
const setNewServersList = (serversList: StoreServerItem[]) => {
|
|
localStorage['serversList'] = JSON.stringify(serversList)
|
|
|
|
// cleanup legacy
|
|
localStorage.removeItem('serverHistory')
|
|
localStorage.removeItem('server')
|
|
localStorage.removeItem('password')
|
|
localStorage.removeItem('version')
|
|
}
|
|
|
|
const getInitialProxies = () => {
|
|
const proxies = [] as string[]
|
|
if (miscUiState.appConfig?.defaultProxy) {
|
|
proxies.push(miscUiState.appConfig.defaultProxy)
|
|
}
|
|
if (localStorage['proxy']) {
|
|
proxies.push(localStorage['proxy'])
|
|
localStorage.removeItem('proxy')
|
|
}
|
|
return proxies
|
|
}
|
|
|
|
export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem) => {
|
|
// function assumes component is not mounted to avoid sync issues after save
|
|
const { loadedServerIndex } = miscUiState
|
|
if (!loadedServerIndex) return
|
|
const servers = getInitialServersList()
|
|
const server = servers[loadedServerIndex]
|
|
servers[loadedServerIndex] = callback(server)
|
|
setNewServersList(servers)
|
|
}
|
|
|
|
// todo move to base
|
|
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')
|
|
|
|
const Inner = () => {
|
|
const [proxies, setProxies] = useState<readonly string[]>(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies())
|
|
const [selectedProxy, setSelectedProxy] = useState(localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
|
|
const [serverEditScreen, setServerEditScreen] = useState<StoreServerItem | true | null>(null) // true for add
|
|
const [defaultUsername, setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('username', defaultUsername)
|
|
}, [defaultUsername])
|
|
|
|
useEffect(() => {
|
|
// TODO! do not unmount on connecting screen
|
|
// if (proxies.length) {
|
|
// localStorage.setItem('proxies', JSON.stringify(proxies))
|
|
// }
|
|
// if (selectedProxy) {
|
|
// localStorage.setItem('selectedProxy', selectedProxy)
|
|
// }
|
|
}, [proxies])
|
|
|
|
const [serversList, setServersList] = useState<StoreServerItem[]>(() => getInitialServersList())
|
|
const [additionalData, setAdditionalData] = useState<Record<string, AdditionalDisplayData>>({})
|
|
|
|
useDidUpdateEffect(() => {
|
|
// save data only on user changes
|
|
setNewServersList(serversList)
|
|
}, [serversList])
|
|
|
|
// by lastJoined
|
|
const serversListSorted = useMemo(() => {
|
|
return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
|
}, [serversList])
|
|
|
|
useEffect(() => {
|
|
const update = async () => {
|
|
for (const server of serversListSorted) {
|
|
const isInLocalNetwork = server.ip.startsWith('192.168.') || server.ip.startsWith('10.') || server.ip.startsWith('172.') || server.ip.startsWith('127.') || server.ip.startsWith('localhost')
|
|
if (isInLocalNetwork) continue
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await fetch(`https://api.mcstatus.io/v2/status/java/${server.ip}`).then(async r => r.json()).then((data: ServerResponse) => {
|
|
const versionClean = data.version?.name_raw.replace(/^[^\d.]+/, '')
|
|
if (!versionClean) return
|
|
setAdditionalData(old => {
|
|
return ({
|
|
...old,
|
|
[server.ip]: {
|
|
formattedText: data.motd?.raw ?? '',
|
|
textNameRight: `${versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}`,
|
|
icon: data.icon,
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
void update()
|
|
}, [serversListSorted])
|
|
|
|
const isEditScreenModal = useIsModalActive('editServer')
|
|
|
|
useDidUpdateEffect(() => {
|
|
if (serverEditScreen && !isEditScreenModal) {
|
|
showModal({ reactType: 'editServer' })
|
|
}
|
|
if (!serverEditScreen && isEditScreenModal) {
|
|
hideCurrentModal()
|
|
}
|
|
}, [serverEditScreen])
|
|
|
|
useDidUpdateEffect(() => {
|
|
if (!isEditScreenModal) {
|
|
setServerEditScreen(null)
|
|
}
|
|
}, [isEditScreenModal])
|
|
|
|
if (isEditScreenModal) {
|
|
return <AddServerOrConnect
|
|
defaults={{
|
|
proxyOverride: selectedProxy,
|
|
usernameOverride: defaultUsername,
|
|
}}
|
|
parseQs={!serverEditScreen}
|
|
onBack={() => {
|
|
hideCurrentModal()
|
|
}}
|
|
onConfirm={(info) => {
|
|
if (!serverEditScreen) return
|
|
if (serverEditScreen === true) {
|
|
const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first
|
|
setServersList(old => [...old, server])
|
|
} else {
|
|
const index = serversList.indexOf(serverEditScreen)
|
|
const { lastJoined } = serversList[index]
|
|
serversList[index] = { ...info, lastJoined }
|
|
setServersList([...serversList])
|
|
}
|
|
setServerEditScreen(null)
|
|
}}
|
|
initialData={!serverEditScreen || serverEditScreen === true ? undefined : serverEditScreen}
|
|
onQsConnect={(info) => {
|
|
const connectOptions: ConnectOptions = {
|
|
username: info.usernameOverride || defaultUsername,
|
|
server: normalizeIp(info.ip),
|
|
proxy: info.proxyOverride || selectedProxy,
|
|
botVersion: info.versionOverride,
|
|
password: info.passwordOverride,
|
|
ignoreQs: true,
|
|
}
|
|
dispatchEvent(new CustomEvent('connect', { detail: connectOptions }))
|
|
}}
|
|
/>
|
|
}
|
|
|
|
return <ServersList
|
|
joinServer={(indexOrIp, overrides) => {
|
|
let ip = indexOrIp
|
|
let server: StoreServerItem | undefined
|
|
if (overrides.shouldSave === undefined) {
|
|
// hack: inner component doesn't know of overrides for existing servers
|
|
server = serversListSorted.find(s => s.index.toString() === indexOrIp)!
|
|
ip = server.ip
|
|
overrides = server
|
|
}
|
|
|
|
const lastJoinedUsername = serversListSorted.find(s => s.usernameOverride)?.usernameOverride
|
|
let username = overrides.username || defaultUsername
|
|
if (!username) {
|
|
username = prompt('Username', lastJoinedUsername || '')
|
|
if (!username) return
|
|
setDefaultUsername(username)
|
|
}
|
|
const options = {
|
|
username,
|
|
server: normalizeIp(ip),
|
|
proxy: overrides.proxy || selectedProxy,
|
|
botVersion: overrides.versionOverride ?? /* legacy */ overrides['version'],
|
|
password: overrides.password,
|
|
ignoreQs: true,
|
|
autoLoginPassword: server?.autoLogin?.[username],
|
|
onSuccessfulPlay () {
|
|
if (overrides.shouldSave && !serversList.some(s => s.ip === ip)) {
|
|
const newServersList: StoreServerItem[] = [...serversList, {
|
|
ip,
|
|
lastJoined: Date.now(),
|
|
versionOverride: overrides.versionOverride,
|
|
}]
|
|
// setServersList(newServersList)
|
|
setNewServersList(newServersList) // component is not mounted
|
|
}
|
|
|
|
if (overrides.shouldSave === undefined) { // loading saved
|
|
// find and update
|
|
const server = serversList.find(s => s.ip === ip)
|
|
if (server) {
|
|
server.lastJoined = Date.now()
|
|
// setServersList([...serversList])
|
|
setNewServersList(serversList) // component is not mounted
|
|
}
|
|
}
|
|
|
|
// save new selected proxy (if new)
|
|
if (!proxies.includes(selectedProxy)) {
|
|
// setProxies([...proxies, selectedProxy])
|
|
localStorage.setItem('proxies', JSON.stringify([...proxies, selectedProxy]))
|
|
}
|
|
if (selectedProxy) {
|
|
localStorage.setItem('selectedProxy', selectedProxy)
|
|
}
|
|
},
|
|
serverIndex: overrides.shouldSave ? serversList.length.toString() : indexOrIp // assume last
|
|
} satisfies ConnectOptions
|
|
dispatchEvent(new CustomEvent('connect', { detail: options }))
|
|
// qsOptions
|
|
}}
|
|
username={defaultUsername}
|
|
setUsername={setDefaultUsername}
|
|
onWorldAction={(action, index) => {
|
|
const server = serversList[index]
|
|
if (!server) return
|
|
|
|
if (action === 'edit') {
|
|
setServerEditScreen(server)
|
|
}
|
|
if (action === 'delete') {
|
|
setServersList(old => old.filter(s => s !== server))
|
|
}
|
|
}}
|
|
onGeneralAction={(action) => {
|
|
if (action === 'create') {
|
|
setServerEditScreen(true)
|
|
}
|
|
if (action === 'cancel') {
|
|
hideCurrentModal()
|
|
}
|
|
}}
|
|
worldData={serversListSorted.map(server => {
|
|
const additional = additionalData[server.ip]
|
|
return {
|
|
name: server.index.toString(),
|
|
title: server.name || server.ip,
|
|
detail: (server.versionOverride ?? '') + ' ' + (server.usernameOverride ?? ''),
|
|
// lastPlayed: server.lastJoined,
|
|
formattedTextOverride: additional?.formattedText,
|
|
worldNameRight: additional?.textNameRight ?? '',
|
|
iconSrc: additional?.icon,
|
|
}
|
|
})}
|
|
initialProxies={{
|
|
proxies,
|
|
selected: selectedProxy,
|
|
}}
|
|
updateProxies={({ proxies, selected }) => {
|
|
// new proxy is saved in joinServer
|
|
setProxies(proxies)
|
|
setSelectedProxy(selected)
|
|
}}
|
|
/>
|
|
}
|
|
|
|
export default () => {
|
|
const editServerModalActive = useIsModalActive('editServer')
|
|
const isServersListModalActive = useIsModalActive('serversList')
|
|
const eitherModal = isServersListModalActive || editServerModalActive
|
|
return eitherModal ? <Inner /> : null
|
|
}
|