2026-03-05 08:05:23 +07:00
|
|
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
2026-05-09 21:29:07 +07:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-04-22 02:45:45 +07:00
|
|
|
import {
|
|
|
|
|
searchLibraries,
|
|
|
|
|
installLibrary,
|
|
|
|
|
getInstalledLibraries,
|
2026-05-02 14:24:09 +07:00
|
|
|
uninstallLibrary,
|
2026-04-22 02:45:45 +07:00
|
|
|
} from '../../services/libraryService';
|
2026-03-05 08:05:23 +07:00
|
|
|
import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService';
|
2026-03-25 13:06:38 +07:00
|
|
|
import { trackInstallLibrary } from '../../utils/analytics';
|
2026-03-05 08:05:23 +07:00
|
|
|
import './LibraryManagerModal.css';
|
|
|
|
|
|
|
|
|
|
interface LibraryManagerModalProps {
|
2026-04-22 02:45:45 +07:00
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
2026-03-05 08:05:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Tab = 'search' | 'installed';
|
|
|
|
|
|
|
|
|
|
export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen, onClose }) => {
|
2026-05-09 21:29:07 +07:00
|
|
|
const { t } = useTranslation();
|
2026-04-22 02:45:45 +07:00
|
|
|
const [activeTab, setActiveTab] = useState<Tab>('search');
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [searchResults, setSearchResults] = useState<ArduinoLibrary[]>([]);
|
|
|
|
|
const [installedLibraries, setInstalledLibraries] = useState<InstalledLibrary[]>([]);
|
|
|
|
|
const [loadingSearch, setLoadingSearch] = useState(false);
|
|
|
|
|
const [loadingInstalled, setLoadingInstalled] = useState(false);
|
|
|
|
|
const [installingLib, setInstallingLib] = useState<string | null>(null);
|
2026-05-02 14:24:09 +07:00
|
|
|
const [uninstallingLib, setUninstallingLib] = useState<string | null>(null);
|
|
|
|
|
/** Track user-selected version per library name */
|
|
|
|
|
const [selectedVersions, setSelectedVersions] = useState<Record<string, string>>({});
|
2026-04-22 02:45:45 +07:00
|
|
|
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
|
|
|
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const fetchInstalled = useCallback(async () => {
|
|
|
|
|
setLoadingInstalled(true);
|
|
|
|
|
try {
|
|
|
|
|
const libs = await getInstalledLibraries();
|
|
|
|
|
setInstalledLibraries(libs);
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
setStatusMsg({
|
|
|
|
|
type: 'error',
|
|
|
|
|
text: e instanceof Error ? e.message : 'Failed to load installed libraries',
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingInstalled(false);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
// Reset state when modal closes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
setSearchQuery('');
|
|
|
|
|
setSearchResults([]);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen]);
|
2026-03-05 11:23:59 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
// Fetch installed list when modal opens or switching to installed tab
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen && activeTab === 'installed') fetchInstalled();
|
|
|
|
|
}, [isOpen, activeTab, fetchInstalled]);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen) fetchInstalled();
|
|
|
|
|
}, [isOpen, fetchInstalled]);
|
2026-03-06 07:07:10 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
// Search: immediate on open (empty query), debounced when typing
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOpen) return;
|
|
|
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
|
|
|
const delay = searchQuery ? 400 : 0;
|
|
|
|
|
debounceRef.current = setTimeout(async () => {
|
|
|
|
|
setLoadingSearch(true);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
try {
|
|
|
|
|
const results = await searchLibraries(searchQuery);
|
|
|
|
|
setSearchResults(results);
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Search failed' });
|
|
|
|
|
setSearchResults([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoadingSearch(false);
|
|
|
|
|
}
|
|
|
|
|
}, delay);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
return () => {
|
|
|
|
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
2026-03-05 08:05:23 +07:00
|
|
|
};
|
2026-04-22 02:45:45 +07:00
|
|
|
}, [searchQuery, isOpen]);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const handleInstall = async (libName: string) => {
|
|
|
|
|
setInstallingLib(libName);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
try {
|
2026-05-02 14:24:09 +07:00
|
|
|
const version = selectedVersions[libName];
|
|
|
|
|
const result = await installLibrary(libName, version);
|
2026-04-22 02:45:45 +07:00
|
|
|
if (result.success) {
|
|
|
|
|
trackInstallLibrary(libName);
|
2026-05-02 14:24:09 +07:00
|
|
|
if (result.fallback) {
|
|
|
|
|
setStatusMsg({ type: 'success', text: `"${libName}" installed (latest — requested @${result.requested_version} was not available)` });
|
|
|
|
|
} else {
|
|
|
|
|
setStatusMsg({ type: 'success', text: `"${libName}${version ? ' @' + version : ''}" installed successfully!` });
|
|
|
|
|
}
|
|
|
|
|
fetchInstalled();
|
2026-04-22 02:45:45 +07:00
|
|
|
} else {
|
|
|
|
|
setStatusMsg({ type: 'error', text: result.error || `Failed to install "${libName}"` });
|
|
|
|
|
}
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Installation failed' });
|
|
|
|
|
} finally {
|
|
|
|
|
setInstallingLib(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-05-02 14:24:09 +07:00
|
|
|
const handleUninstall = async (libName: string) => {
|
|
|
|
|
setUninstallingLib(libName);
|
|
|
|
|
setStatusMsg(null);
|
|
|
|
|
try {
|
|
|
|
|
const result = await uninstallLibrary(libName);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setStatusMsg({ type: 'success', text: `"${libName}" uninstalled successfully!` });
|
|
|
|
|
fetchInstalled();
|
|
|
|
|
} else {
|
|
|
|
|
setStatusMsg({ type: 'error', text: result.error || `Failed to uninstall "${libName}"` });
|
|
|
|
|
}
|
|
|
|
|
} catch (e: unknown) {
|
|
|
|
|
setStatusMsg({ type: 'error', text: e instanceof Error ? e.message : 'Uninstall failed' });
|
|
|
|
|
} finally {
|
|
|
|
|
setUninstallingLib(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const handleClose = () => {
|
|
|
|
|
onClose();
|
|
|
|
|
};
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
if (!isOpen) return null;
|
2026-03-05 11:23:59 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const isInstalled = (libName: string): boolean =>
|
|
|
|
|
installedLibraries.some(
|
|
|
|
|
(il) => (il.library?.name || il.name || '').toLowerCase() === libName.toLowerCase(),
|
|
|
|
|
);
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const getLibName = (lib: ArduinoLibrary): string => lib.name || 'Unknown';
|
|
|
|
|
const getLibVersion = (lib: ArduinoLibrary): string => lib.latest?.version || lib.version || '';
|
|
|
|
|
const getLibAuthor = (lib: ArduinoLibrary): string => lib.latest?.author || lib.author || '';
|
|
|
|
|
const getLibDesc = (lib: ArduinoLibrary): string => lib.latest?.sentence || lib.sentence || '';
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
const getInstalledName = (lib: InstalledLibrary): string =>
|
|
|
|
|
lib.library?.name || lib.name || 'Unknown';
|
|
|
|
|
const getInstalledVersion = (lib: InstalledLibrary): string =>
|
|
|
|
|
lib.library?.version || lib.version || '';
|
|
|
|
|
const getInstalledAuthor = (lib: InstalledLibrary): string =>
|
|
|
|
|
lib.library?.author || lib.author || '';
|
|
|
|
|
const getInstalledDesc = (lib: InstalledLibrary): string =>
|
|
|
|
|
lib.library?.sentence || lib.sentence || '';
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
return (
|
|
|
|
|
<div className="lib-modal-overlay" onClick={handleClose}>
|
|
|
|
|
<div className="lib-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="lib-modal-header">
|
|
|
|
|
<div className="lib-modal-title">
|
|
|
|
|
<svg
|
|
|
|
|
className="lib-icon"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
|
|
|
|
|
<path d="m3.3 7 8.7 5 8.7-5" />
|
|
|
|
|
<path d="M12 22V12" />
|
|
|
|
|
</svg>
|
2026-05-09 21:29:07 +07:00
|
|
|
<span>{t('editor.libraryManager.title')}</span>
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
<button className="lib-close-btn" onClick={handleClose}>
|
|
|
|
|
<svg
|
|
|
|
|
width="14"
|
|
|
|
|
height="14"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
>
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{/* Tabs */}
|
|
|
|
|
<div className="lib-tabs">
|
|
|
|
|
<button
|
|
|
|
|
className={`lib-tab ${activeTab === 'search' ? 'active' : ''}`}
|
|
|
|
|
onClick={() => setActiveTab('search')}
|
|
|
|
|
>
|
2026-05-09 21:29:07 +07:00
|
|
|
{t('editor.libraryManager.searchTab')}
|
2026-04-22 02:45:45 +07:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className={`lib-tab ${activeTab === 'installed' ? 'active' : ''}`}
|
|
|
|
|
onClick={() => setActiveTab('installed')}
|
|
|
|
|
>
|
2026-05-09 21:29:07 +07:00
|
|
|
{t('editor.libraryManager.installedTab')}
|
2026-04-22 02:45:45 +07:00
|
|
|
</button>
|
|
|
|
|
</div>
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{/* Status bar */}
|
|
|
|
|
{statusMsg && (
|
|
|
|
|
<div className={`lib-status ${statusMsg.type}`}>
|
|
|
|
|
{statusMsg.type === 'success' ? (
|
|
|
|
|
<svg
|
|
|
|
|
width="14"
|
|
|
|
|
height="14"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
style={{ flexShrink: 0 }}
|
|
|
|
|
>
|
|
|
|
|
<polyline points="20 6 9 17 4 12" />
|
|
|
|
|
</svg>
|
|
|
|
|
) : (
|
|
|
|
|
<svg
|
|
|
|
|
width="14"
|
|
|
|
|
height="14"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
style={{ flexShrink: 0 }}
|
|
|
|
|
>
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
|
|
|
|
{statusMsg.text}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{/* Search Tab */}
|
|
|
|
|
{activeTab === 'search' && (
|
|
|
|
|
<div className="lib-content">
|
|
|
|
|
<div className="lib-search-bar">
|
|
|
|
|
<svg
|
|
|
|
|
className="lib-search-icon"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<circle cx="11" cy="11" r="8" />
|
|
|
|
|
<path d="m21 21-4.3-4.3" />
|
|
|
|
|
</svg>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
2026-05-09 21:29:07 +07:00
|
|
|
placeholder={t('editor.libraryManager.filterPlaceholder')}
|
2026-04-22 02:45:45 +07:00
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{loadingSearch && (
|
|
|
|
|
<svg
|
|
|
|
|
className="lib-spinner"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
>
|
|
|
|
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="lib-list">
|
|
|
|
|
{loadingSearch && (
|
|
|
|
|
<div className="lib-empty">
|
|
|
|
|
<svg
|
|
|
|
|
className="lib-spinner lib-spinner-center"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2.5"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
>
|
|
|
|
|
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="lib-empty-sub">
|
2026-05-09 21:29:07 +07:00
|
|
|
{searchQuery
|
|
|
|
|
? t('editor.libraryManager.searchingFor', { query: searchQuery })
|
|
|
|
|
: t('editor.libraryManager.loadingLibraries')}
|
2026-04-22 02:45:45 +07:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!loadingSearch && searchResults.length === 0 && (
|
|
|
|
|
<div className="lib-empty">
|
|
|
|
|
<p>
|
|
|
|
|
{searchQuery
|
2026-05-09 21:29:07 +07:00
|
|
|
? t('editor.libraryManager.noResultsFor', { query: searchQuery })
|
|
|
|
|
: t('editor.libraryManager.noResults')}
|
2026-04-22 02:45:45 +07:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!loadingSearch &&
|
|
|
|
|
searchResults.map((lib, i) => (
|
|
|
|
|
<div key={i} className="lib-item">
|
|
|
|
|
<div className="lib-item-info">
|
|
|
|
|
<div className="lib-item-header">
|
|
|
|
|
<span className="lib-item-name">{getLibName(lib)}</span>
|
|
|
|
|
{getLibAuthor(lib) && (
|
2026-05-09 21:29:07 +07:00
|
|
|
<span className="lib-item-author">
|
|
|
|
|
{t('editor.libraryManager.byAuthor', { author: getLibAuthor(lib) })}
|
|
|
|
|
</span>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{getLibDesc(lib) && <p className="lib-item-desc">{getLibDesc(lib)}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="lib-item-actions">
|
|
|
|
|
{isInstalled(getLibName(lib)) ? (
|
2026-05-02 14:24:09 +07:00
|
|
|
<>
|
|
|
|
|
<span className="lib-item-version lib-installed-badge">
|
|
|
|
|
{selectedVersions[lib.name] ?? lib.latest?.version ?? ''}
|
|
|
|
|
<svg style={{ display: 'inline', marginLeft: '4px', verticalAlign: 'middle' }} width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12" /></svg>
|
|
|
|
|
</span>
|
|
|
|
|
<button
|
|
|
|
|
className="lib-uninstall-btn"
|
|
|
|
|
onClick={() => handleUninstall(getLibName(lib))}
|
|
|
|
|
disabled={uninstallingLib !== null}
|
2026-04-22 02:45:45 +07:00
|
|
|
>
|
2026-05-09 21:29:07 +07:00
|
|
|
{uninstallingLib === getLibName(lib)
|
|
|
|
|
? t('editor.libraryManager.uninstalling')
|
|
|
|
|
: t('editor.libraryManager.uninstall')}
|
2026-05-02 14:24:09 +07:00
|
|
|
</button>
|
|
|
|
|
</>
|
2026-04-22 02:45:45 +07:00
|
|
|
) : (
|
2026-05-02 14:24:09 +07:00
|
|
|
<>
|
|
|
|
|
{lib.releases && Object.keys(lib.releases).length > 1 && (
|
|
|
|
|
<select
|
|
|
|
|
className="lib-version-select"
|
|
|
|
|
value={selectedVersions[lib.name] ?? lib.latest?.version ?? ''}
|
|
|
|
|
onChange={(e) => setSelectedVersions((prev) => ({ ...prev, [lib.name]: e.target.value }))}
|
|
|
|
|
>
|
|
|
|
|
{Object.entries(lib.releases).map(([ver]) => (
|
|
|
|
|
<option key={ver} value={ver}>{ver}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
2026-05-02 14:24:09 +07:00
|
|
|
<button
|
|
|
|
|
className="lib-install-btn"
|
|
|
|
|
onClick={() => handleInstall(getLibName(lib))}
|
|
|
|
|
disabled={installingLib !== null}
|
|
|
|
|
>
|
|
|
|
|
{installingLib === getLibName(lib) ? (
|
2026-05-09 21:29:07 +07:00
|
|
|
<span className="lib-installing">{t('editor.libraryManager.installing')}</span>
|
2026-05-02 14:24:09 +07:00
|
|
|
) : (
|
2026-05-09 21:29:07 +07:00
|
|
|
t('editor.libraryManager.install')
|
2026-05-02 14:24:09 +07:00
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
2026-03-05 08:05:23 +07:00
|
|
|
</div>
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-05 08:05:23 +07:00
|
|
|
|
2026-04-22 02:45:45 +07:00
|
|
|
{/* Installed Tab */}
|
|
|
|
|
{activeTab === 'installed' && (
|
|
|
|
|
<div className="lib-content">
|
|
|
|
|
<div className="lib-list">
|
|
|
|
|
{loadingInstalled && (
|
|
|
|
|
<div className="lib-empty">
|
2026-05-09 21:29:07 +07:00
|
|
|
<p>{t('editor.libraryManager.loadingInstalled')}</p>
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!loadingInstalled && installedLibraries.length === 0 && (
|
|
|
|
|
<div className="lib-empty">
|
2026-05-09 21:29:07 +07:00
|
|
|
<p>{t('editor.libraryManager.noneInstalled')}</p>
|
|
|
|
|
<p className="lib-empty-sub">{t('editor.libraryManager.useSearchTab')}</p>
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{installedLibraries.map((lib, i) => (
|
|
|
|
|
<div key={i} className="lib-item">
|
|
|
|
|
<div className="lib-item-info">
|
|
|
|
|
<div className="lib-item-header">
|
|
|
|
|
<span className="lib-item-name">{getInstalledName(lib)}</span>
|
|
|
|
|
{getInstalledAuthor(lib) && (
|
2026-05-09 21:29:07 +07:00
|
|
|
<span className="lib-item-author">
|
|
|
|
|
{t('editor.libraryManager.byAuthor', { author: getInstalledAuthor(lib) })}
|
|
|
|
|
</span>
|
2026-04-22 02:45:45 +07:00
|
|
|
)}
|
2026-03-05 08:05:23 +07:00
|
|
|
</div>
|
2026-04-22 02:45:45 +07:00
|
|
|
{getInstalledDesc(lib) && (
|
|
|
|
|
<p className="lib-item-desc">{getInstalledDesc(lib)}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="lib-item-actions">
|
|
|
|
|
{getInstalledVersion(lib) && (
|
|
|
|
|
<span className="lib-item-version lib-installed-badge">
|
|
|
|
|
{getInstalledVersion(lib)}
|
|
|
|
|
<svg
|
|
|
|
|
style={{ display: 'inline', marginLeft: '4px', verticalAlign: 'middle' }}
|
|
|
|
|
width="12"
|
|
|
|
|
height="12"
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="3"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<polyline points="20 6 9 17 4 12" />
|
|
|
|
|
</svg>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-05-02 14:24:09 +07:00
|
|
|
<button
|
|
|
|
|
className="lib-uninstall-btn"
|
|
|
|
|
onClick={() => handleUninstall(getInstalledName(lib))}
|
|
|
|
|
disabled={uninstallingLib !== null}
|
|
|
|
|
>
|
|
|
|
|
{uninstallingLib === getInstalledName(lib) ? 'Uninstalling...' : 'UNINSTALL'}
|
|
|
|
|
</button>
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-03-05 08:05:23 +07:00
|
|
|
</div>
|
2026-04-22 02:45:45 +07:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-03-05 08:05:23 +07:00
|
|
|
};
|