diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx index e3fb2a153..0287779bd 100644 --- a/src/react/AddServerOrConnect.tsx +++ b/src/react/AddServerOrConnect.tsx @@ -3,7 +3,7 @@ import Screen from './Screen' import Input from './Input' import Button from './Button' import SelectGameVersion from './SelectGameVersion' -import { useIsSmallWidth } from './simpleHooks' +import { useIsSmallWidth, usePassesWindowDimensions } from './simpleHooks' export interface BaseServerInfo { ip: string @@ -32,6 +32,7 @@ interface Props { const ELEMENTS_WIDTH = 190 export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => { + const isSmallHeight = !usePassesWindowDimensions(null, 350) const qsParams = parseQs ? new URLSearchParams(window.location.search) : undefined const qsParamName = qsParams?.get('name') const qsParamIp = qsParams?.get('ip') @@ -101,7 +102,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ } setServerIp(value)} /> setServerPort(value)} placeholder='25565' /> -
Overrides:
+ {isSmallHeight ?
:
Overrides:
}
ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '') +const FETCH_DELAY = 100 // ms between each request +const MAX_CONCURRENT_REQUESTS = 10 + const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersList?: string[] }) => { const [proxies, setProxies] = useState(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies()) const [selectedProxy, setSelectedProxy] = useState(proxyQs ?? localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '') @@ -198,30 +202,69 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL useUtilsEffect(({ signal }) => { 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 || signal.aborted) continue - // eslint-disable-next-line no-await-in-loop - await fetch(`https://api.mcstatus.io/v2/status/java/${server.ip}`, { - // TODO: bounty for this who fix it - // signal - }).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, - } - }) - }) + const queue = serversListSorted + .map(server => { + const isInLocalNetwork = server.ip.startsWith('192.168.') || + server.ip.startsWith('10.') || + server.ip.startsWith('172.') || + server.ip.startsWith('127.') || + server.ip.startsWith('localhost') || + server.ip.startsWith(':') + + const VALID_IP_OR_DOMAIN = server.ip.includes('.') + if (isInLocalNetwork || signal.aborted || !VALID_IP_OR_DOMAIN) return null + + return server + }) + .filter(x => x !== null) + + const activeRequests = new Set>() + + let lastRequestStart = 0 + for (const server of queue) { + // Wait if at concurrency limit + if (activeRequests.size >= MAX_CONCURRENT_REQUESTS) { + // eslint-disable-next-line no-await-in-loop + await Promise.race(activeRequests) + } + + // Create and track new request + // eslint-disable-next-line @typescript-eslint/no-loop-func + const request = new Promise(resolve => { + setTimeout(async () => { + try { + lastRequestStart = Date.now() + if (signal.aborted) return + const response = await fetch(`https://api.mcstatus.io/v2/status/java/${server.ip}`, { signal }) + const data: ServerResponse = await response.json() + const versionClean = data.version?.name_raw.replace(/^[^\d.]+/, '') + + setAdditionalData(old => ({ + ...old, + [server.ip]: { + formattedText: data.motd?.raw ?? '', + textNameRight: data.online ? + `${versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}` : + '', + icon: data.icon, + offline: !data.online + } + })) + } finally { + activeRequests.delete(request) + resolve() + } + }, lastRequestStart ? Math.max(0, FETCH_DELAY - (Date.now() - lastRequestStart)) : 0) }) + + activeRequests.add(request) } + + // Wait for remaining requests + await Promise.all(activeRequests) } - void update().catch((err) => {}) + + void update() }, [serversListSorted]) const isEditScreenModal = useIsModalActive('editServer') @@ -394,10 +437,10 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL 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, + offline: additional?.offline } })} initialProxies={{ diff --git a/src/react/Singleplayer.tsx b/src/react/Singleplayer.tsx index 79c08ab80..d8291d907 100644 --- a/src/react/Singleplayer.tsx +++ b/src/react/Singleplayer.tsx @@ -12,6 +12,7 @@ import Button from './Button' import Tabs from './Tabs' import MessageFormattedString from './MessageFormattedString' import { useIsSmallWidth } from './simpleHooks' +import PixelartIcon from './PixelartIcon' export interface WorldProps { name: string @@ -26,9 +27,10 @@ export interface WorldProps { onFocus?: (name: string) => void onInteraction?(interaction: 'enter' | 'space') elemRef?: React.Ref + offline?: boolean } -const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef }: WorldProps & { ref?: React.Ref }) => { +const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef, offline }: WorldProps & { ref?: React.Ref }) => { const timeRelativeFormatted = useMemo(() => { if (!lastPlayed) return '' const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) @@ -60,7 +62,14 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
{title}
-
{worldNameRight}
+
+ {offline ? ( + + + Offline + + ) : worldNameRight} +
{formattedTextOverride ?
diff --git a/src/react/simpleHooks.ts b/src/react/simpleHooks.ts index ec9c88e11..20607f15b 100644 --- a/src/react/simpleHooks.ts +++ b/src/react/simpleHooks.ts @@ -7,6 +7,18 @@ export const useIsSmallWidth = () => { return useMedia(SMALL_SCREEN_MEDIA.replace('@media ', '')) } +export const usePassesWindowDimensions = (minWidth: number | null = null, minHeight: number | null = null) => { + let media = '(' + if (minWidth !== null) { + media += `min-width: ${minWidth}px, ` + } + if (minHeight !== null) { + media += `min-height: ${minHeight}px, ` + } + media += ')' + return useMedia(media) +} + export const useCopyKeybinding = (getCopyText: () => string | undefined) => { useUtilsEffect(({ signal }) => { addEventListener('keydown', (e) => {