import { useEffect, useMemo, useState, type CSSProperties, } from 'react'; import { autoServer, filterServers, groupServers, parseServerPingResults, SERVER_RESULT_WINDOW, } from './serverPickerModel.js'; import type { HarborServer } from '../../../shared/contracts/state.js'; type PickerServer = HarborServer & { country?: string; city?: string; provider?: string; }; const FAVORITES_KEY = 'harbor-server-favorites'; const RECENT_KEY = 'harbor-server-recent'; const AUTO_KEY = 'harbor-server-auto'; const INLINE_SERVER_ROWS = 8; interface PingResult { id?: string; latency?: number | null; ok?: boolean; error?: unknown; checkedAt?: string; checking?: boolean; [key: string]: unknown; } type PingState = Record; function readList(key: string) { try { const value = JSON.parse(localStorage.getItem(key) || '[]'); return Array.isArray(value) ? value.map(String) : []; } catch { return []; } } function write(key: string, value: string | string[]) { try { localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); } catch { // Preferences remain available for this session. } } function readAuto(key: string) { try { return localStorage.getItem(key) === 'true'; } catch { return false; } } function serverHealthText(ping?: PingResult) { if (ping?.error) return 'Проверка недоступна'; if (ping?.ok) return `${ping.latency} мс`; return ping ? 'Недоступен' : null; } function ServerHealth({ ping, fallback }: { ping?: PingResult; fallback?: string }) { const health = fallback || serverHealthText(ping); if (!health && !ping?.checking) return null; return ; } function ServerCheckButton({ checking, disabled, onClick, }: { checking: boolean; disabled?: boolean; onClick: () => void; }) { return ; } function ServerRow({ server, selected, favorite, ping, disabled, index, onSelect, onFavorite, }: { server: PickerServer; selected: boolean; favorite?: boolean; ping?: PingResult; disabled: boolean; index: number; onSelect: (id: string) => unknown; onFavorite?: (id: string) => void; }) { const health = ping?.checking ? 'Проверяем пинг' : serverHealthText(ping); return
{onFavorite && }
; } interface ServerPickerProps { profileId: string; pingServers: (profileId: string, ids: string[]) => Promise; servers: PickerServer[]; selectedServerId: string; disabled: boolean; prompt: boolean; leaving: boolean; revealVersion: number; anchorServerId?: string; onSelect: (id: string) => unknown; } export function ServerPicker({ profileId, pingServers, servers, selectedServerId, disabled, prompt, leaving, revealVersion, anchorServerId = '', onSelect, }: ServerPickerProps) { const favoritesKey = `${FAVORITES_KEY}:${profileId}`; const recentKey = `${RECENT_KEY}:${profileId}`; const autoKey = `${AUTO_KEY}:${profileId}`; const [query, setQuery] = useState(''); const [advanced, setAdvanced] = useState(false); const [view, setView] = useState<'all' | 'favorites' | 'recent'>('all'); const [page, setPage] = useState(0); const [favorites, setFavorites] = useState(() => readList(favoritesKey)); const [recent, setRecent] = useState(() => readList(recentKey)); const [autoActive, setAutoActive] = useState(() => readAuto(autoKey)); const [collapsed, setCollapsed] = useState([]); const [pings, setPings] = useState({}); const [checking, setChecking] = useState(false); const serverKey = servers.map(({ id }) => id).join('|'); useEffect(() => { setPage(0); }, [query, view, serverKey]); const selected = servers.find(({ id }) => id === selectedServerId); const filtered = useMemo(() => { const found = filterServers(servers, query) as PickerServer[]; if (view === 'favorites') return found.filter(({ id }) => favorites.includes(id)); if (view === 'recent') return recent.flatMap((id) => found.find((server) => server.id === id) || []); return found; }, [servers, query, view, favorites, recent]); const results = filtered.filter(({ id }) => id !== selectedServerId && id !== anchorServerId); const pageCount = Math.max(1, Math.ceil(results.length / SERVER_RESULT_WINDOW)); const visible = results.slice(page * SERVER_RESULT_WINDOW, (page + 1) * SERVER_RESULT_WINDOW); const grouped = servers.length >= 10; useEffect(() => { setPage((current) => Math.min(current, pageCount - 1)); }, [pageCount]); function toggleFavorite(id: string) { setFavorites((current) => { const next = current.includes(id) ? current.filter((item) => item !== id) : [id, ...current]; write(favoritesKey, next); return next; }); } function select(id: string, automatic = false) { setAutoActive(automatic); write(autoKey, String(automatic)); if (!automatic) { setRecent((current) => { const next = [id, ...current.filter((item) => item !== id)].slice(0, 5); write(recentKey, next); return next; }); } onSelect(id); } async function checkVisible() { const ids = [...new Set([selectedServerId, ...visible.map(({ id }) => id)].filter(Boolean))].slice(0, 30); if (!ids.length) return; const startedAt = performance.now(); setChecking(true); setPings((current) => ({ ...current, ...Object.fromEntries(ids.map((id) => [id, { ...current[id], checking: true }])), })); try { const results = parseServerPingResults(await pingServers(profileId, ids)) as PingResult[]; setPings((current) => ({ ...current, ...Object.fromEntries(results.map((result) => [result.id, { ...result, checking: true }])), })); } catch { setPings((current) => ({ ...current, ...Object.fromEntries(ids.map((id) => [id, { error: true, checking: true, checkedAt: new Date().toISOString(), }])), })); } finally { const elapsed = performance.now() - startedAt; const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; const completeAt = reduced ? elapsed : Math.max(900, Math.ceil(elapsed / 900) * 900); await new Promise((resolve) => setTimeout(resolve, Math.max(0, completeAt - elapsed))); setPings((current) => ({ ...current, ...Object.fromEntries(ids.map((id) => [id, { ...current[id], checking: false }])), })); setChecking(false); } } if (servers.length === 1) { return
{prompt && Выберите сервер}
Список серверов
{servers[0].id !== anchorServerId && }
; } const renderRows = (items: PickerServer[], offset = 0) => items.map((server, index) => ( )); const simpleServers = [ ...(selected && selected.id !== anchorServerId ? [selected] : []), ...servers.filter(({ id }) => id !== selectedServerId && id !== anchorServerId), ]; return
{prompt && Выберите сервер}
Список серверов
{simpleServers.map((server, index) => )}
setQuery(event.target.value)} />
{([ ['all', 'Все'], ['favorites', '★'], ['recent', 'Недавние'], ] as const).map(([id, label]) => )}
{selected && selected.id !== anchorServerId && }
{!visible.length &&

Серверы не найдены

} {grouped ? groupServers(visible).map(([group, items]) => { const isCollapsed = collapsed.includes(group); return
{!isCollapsed &&
{renderRows(items)}
}
; }) :
{renderRows(visible)}
} {pageCount > 1 && }
; }