import classNames from 'classnames' import { Fragment, useEffect, useMemo, useRef, useState } from 'react' // todo optimize size import missingWorldPreview from 'mc-assets/dist/other-textures/latest/gui/presets/isles.png' import { filesize } from 'filesize' import useTypedEventListener from 'use-typed-event-listener' import { focusable } from 'tabbable' import styles from './singleplayer.module.css' import Input from './Input' 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 title: string size?: number lastPlayed?: number isFocused?: boolean iconSrc?: string detail?: string formattedTextOverride?: string worldNameRight?: string 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, offline }: WorldProps & { ref?: React.Ref }) => { const timeRelativeFormatted = useMemo(() => { if (!lastPlayed) return '' const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) const diff = Date.now() - lastPlayed const minutes = Math.floor(diff / 1000 / 60) const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) // const weeks = Math.floor(days / 7) // const months = Math.floor(days / 30) if (days > 0) return formatter.format(-days, 'day') if (hours > 0) return formatter.format(-hours, 'hour') return formatter.format(-minutes, 'minute') }, [lastPlayed]) const sizeFormatted = useMemo(() => { if (!size) return '' return filesize(size) }, [size]) return
onFocus?.(name)} onKeyDown={(e) => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() onInteraction?.(e.code === 'Enter' ? 'enter' : 'space') } }} onDoubleClick={() => onInteraction?.('enter')} > world preview
{title}
{offline ? ( Offline ) : worldNameRight?.startsWith('ws') ? ( {worldNameRight.slice(3)} ) : worldNameRight}
{formattedTextOverride ?
: <>
{timeRelativeFormatted} {detail.slice(-30)}
{sizeFormatted}
}
} interface Props { worldData: WorldProps[] | null // null means loading serversLayout?: boolean firstRowChildrenOverride?: React.ReactNode searchRowChildrenOverride?: React.ReactNode providers?: Record activeProvider?: string setActiveProvider?: (provider: string) => void providerActions?: Record void) | undefined | JSX.Element> disabledProviders?: string[] isReadonly?: boolean error?: string warning?: string warningAction?: () => void warningActionLabel?: string hidden?: boolean onWorldAction (action: 'load' | 'export' | 'delete' | 'edit', worldName: string): void onGeneralAction (action: 'cancel' | 'create'): void onRowSelect? (name: string, index: number): void defaultSelectedRow?: number selectedRow?: number listStyle?: React.CSSProperties setListHovered?: (hovered: boolean) => void secondRowStyles?: React.CSSProperties lockedEditing?: boolean } export default ({ worldData, onGeneralAction, onWorldAction, firstRowChildrenOverride, serversLayout, searchRowChildrenOverride, activeProvider, setActiveProvider, providerActions, providers = {}, disabledProviders, error, isReadonly, warning, warningAction, warningActionLabel, hidden, onRowSelect, defaultSelectedRow, selectedRow, listStyle, setListHovered, secondRowStyles, lockedEditing }: Props) => { const containerRef = useRef() const firstButton = useRef(null) const worldRefs = useRef>({}) useTypedEventListener(window, 'keydown', (e) => { if ((e.code === 'ArrowDown' || e.code === 'ArrowUp')) { e.preventDefault() const dir = e.code === 'ArrowDown' ? 1 : -1 const elements = focusable(containerRef.current) const focusedElemIndex = elements.indexOf(document.activeElement as HTMLElement) if (focusedElemIndex === -1) return const nextElem = elements[focusedElemIndex + dir] nextElem?.focus() } }) const [search, setSearch] = useState('') const [focusedWorld, setFocusedWorld] = useState(defaultSelectedRow === undefined ? '' : worldData?.[defaultSelectedRow]?.name ?? '') useEffect(() => { setFocusedWorld('') }, [activeProvider]) useEffect(() => { if (selectedRow === undefined) return const worldName = worldData?.[selectedRow]?.name setFocusedWorld(worldName ?? '') if (worldName) { worldRefs.current[worldName]?.focus() } }, [selectedRow, worldData?.[selectedRow as any]?.name]) const onRowSelectHandler = (name: string, index: number) => { onRowSelect?.(name, index) setFocusedWorld(name) } const isSmallWidth = useIsSmallWidth() return