import React, { useEffect, useMemo, useState } from 'react'; import { api } from '../api.js'; import { autoServer, filterServers, groupServers, SERVER_RESULT_WINDOW, } from '../utils/serverPicker.js'; const FAVORITES_KEY = 'harbor-server-favorites'; const RECENT_KEY = 'harbor-server-recent'; const AUTO_KEY = 'harbor-server-auto'; function readList(key) { try { const value = JSON.parse(localStorage.getItem(key) || '[]'); return Array.isArray(value) ? value.map(String) : []; } catch { return []; } } function write(key, value) { try { localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); } catch { // Preferences remain available for this session. } } function readAuto() { try { return localStorage.getItem(AUTO_KEY) === 'true'; } catch { return false; } } function ServerRow({ server, selected, favorite, ping, disabled, index, onSelect, onFavorite }) { const health = ping?.checking ? 'Проверяем TCP…' : ping?.error ? 'Проверка недоступна' : ping?.ok ? `TCP ${ping.latency} мс` : ping ? 'TCP недоступен' : 'Не проверен'; return
{onFavorite && }
; } export function ServerPicker({ servers, selectedServerId, disabled, prompt, leaving, revealVersion, onSelect, }) { const [query, setQuery] = useState(''); const [view, setView] = useState('all'); const [page, setPage] = useState(0); const [favorites, setFavorites] = useState(() => readList(FAVORITES_KEY)); const [recent, setRecent] = useState(() => readList(RECENT_KEY)); const [autoActive, setAutoActive] = useState(readAuto); 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); 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); 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) { setFavorites((current) => { const next = current.includes(id) ? current.filter((item) => item !== id) : [id, ...current]; write(FAVORITES_KEY, next); return next; }); } function select(id, automatic = false) { setAutoActive(automatic); write(AUTO_KEY, String(automatic)); if (!automatic) { setRecent((current) => { const next = [id, ...current.filter((item) => item !== id)].slice(0, 5); write(RECENT_KEY, 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; setChecking(true); setPings((current) => ({ ...current, ...Object.fromEntries(ids.map((id) => [id, { checking: true }])), })); try { const data = await api.servers.ping(ids); setPings((current) => ({ ...current, ...Object.fromEntries((data.results || []).map((result) => [result.id, result])), })); } catch { setPings((current) => ({ ...current, ...Object.fromEntries(ids.map((id) => [id, { error: true, checkedAt: new Date().toISOString() }])), })); } finally { setChecking(false); } } if (servers.length === 1) { return
{prompt && Выберите сервер}
; } const renderRows = (items, offset = 0) => items.map((server, index) => ( )); return
{prompt && Выберите сервер}
setQuery(event.target.value)} />
{[ ['all', 'Все'], ['favorites', '★'], ['recent', 'Недавние'], ].map(([id, label]) => )}
{selected && !autoActive && }
{!visible.length &&

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

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