import { useEffect, useMemo, useRef, useState } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import { Search, ExternalLink } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { open as openExternal } from '@tauri-apps/plugin-shell'; import { HIGHLIGHTED_DEPENDENCIES, loadLicensesData, type LicenseEntry, type LicensesData, } from '../utils/licensesData'; import LicenseTextModal from './LicenseTextModal'; const ROW_HEIGHT = 56; function formatDate(iso: string, locale: string): string { try { return new Date(iso).toLocaleDateString(locale, { year: 'numeric', month: 'short', day: 'numeric', }); } catch { return iso; } } function entryKey(e: LicenseEntry): string { return `${e.source}:${e.name}@${e.version}`; } function LicenseRow({ entry, onSelect, onOpenRepo, }: { entry: LicenseEntry; onSelect: (e: LicenseEntry) => void; onOpenRepo: (url: string) => void; }) { const repo = entry.repository || entry.homepage; return ( ); } export default function LicensesPanel() { const { t, i18n } = useTranslation(); const [data, setData] = useState(null); const [error, setError] = useState(null); const [query, setQuery] = useState(''); const [selected, setSelected] = useState(null); useEffect(() => { let cancelled = false; loadLicensesData() .then((d) => { if (!cancelled) setData(d); }) .catch((e) => { if (!cancelled) setError(String(e?.message ?? e)); }); return () => { cancelled = true; }; }, []); const highlights = useMemo(() => { if (!data) return []; const byKey = new Map(); for (const e of data.entries) { byKey.set(`${e.source}:${e.name}`, e); } return HIGHLIGHTED_DEPENDENCIES.map((h) => byKey.get(`${h.source}:${h.name}`)).filter( (e): e is LicenseEntry => e != null, ); }, [data]); const filtered = useMemo(() => { if (!data) return []; const q = query.trim().toLowerCase(); if (!q) return data.entries; return data.entries.filter((e) => { if (e.name.toLowerCase().includes(q)) return true; if (e.version.toLowerCase().includes(q)) return true; for (const lid of e.licenses) { if (lid.toLowerCase().includes(q)) return true; } if (e.source.toLowerCase().includes(q)) return true; return false; }); }, [data, query]); const scrollParentRef = useRef(null); const virtualizer = useVirtualizer({ count: filtered.length, getScrollElement: () => scrollParentRef.current, estimateSize: () => ROW_HEIGHT, overscan: 12, }); const openRepo = (url: string) => { openExternal(url).catch(() => {}); }; if (error) { return (
{t('licenses.loadError')} {error}
); } if (!data) { return (
{t('licenses.loading')}
); } return (
{t('licenses.intro')}
{/* Highlight block */} {highlights.length > 0 && (
{t('licenses.highlights')}
{highlights.map((e) => ( ))}
)} {/* Search */}
setQuery(e.target.value)} style={{ flex: 1, background: 'transparent', border: 'none', outline: 'none', padding: '2px 0', color: 'var(--text-primary)', fontSize: '0.85rem', }} />
{/* Virtual list */}
{filtered.length === 0 ? (
{t('licenses.noResults')}
) : (
{virtualizer.getVirtualItems().map((vi) => { const entry = filtered[vi.index]; return (
); })}
)}
{t('licenses.totalLine', { total: data.stats.total, cargo: data.stats.cargo, npm: data.stats.npm, })} · {t('licenses.generatedAt', { date: formatDate(data.generatedAt, i18n.language) })}
setSelected(null)} />
); }