import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Avatar, Badge, Box, Button, color, config, Header, IconButton, Input, Menu, MenuItem, PopOut, RectCords, Scroll, Spinner, Switch, Text, toRem } from 'folds'; import { Icon, Icons } from '../../../components/icons'; import { HexColorPicker } from 'react-colorful'; import FocusTrap from 'focus-trap-react'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { PluginTab, PluginMetadata, InstalledPlugin } from './types'; import { SequenceCard } from '../../../components/sequence-card'; import { SettingTile } from '../../../components/setting-tile'; import { SequenceCardStyle } from '../styles.css'; import { pluginMarketplaceManager, pluginRegistry, SettingDefinition } from './PluginAPI'; import { stopPropagation } from '../../../utils/keyboard'; import { fuzzyFilter } from '../../../utils/fuzzy'; /** * Renders settings UI for a plugin based on its settings schema */ function PluginSettingsRenderer({ pluginId }: { pluginId: string }) { const schema = pluginRegistry.getPluginSettingsSchema(pluginId); const [, forceUpdate] = useState({}); if (!schema || Object.keys(schema).length === 0) { return ( No settings available for this plugin. ); } const handleChange = (key: string, value: any) => { pluginRegistry.setPluginSetting(pluginId, key, value); forceUpdate({}); }; return ( {Object.entries(schema).map(([key, def]) => ( handleChange(key, value)} /> ))} ); } /** * Renders a single setting based on its type */ function SettingRenderer({ settingKey, definition, value, onChange, }: { settingKey: string; definition: SettingDefinition; value: any; onChange: (value: any) => void; }) { const [colorMenuCords, setColorMenuCords] = useState(); const [selectMenuCords, setSelectMenuCords] = useState(); const label = definition.label || settingKey; const description = definition.description; // Boolean - Switch if (definition.type === 'boolean') { return ( } /> ); } // String - Input if (definition.type === 'string') { return ( onChange(e.target.value)} placeholder={definition.default} /> ); } // Number - Input if (definition.type === 'number') { return ( onChange(parseFloat(e.target.value) || 0)} /> ); } // Select - Dropdown if (definition.type === 'select' && definition.options) { const currentValue = value ?? definition.default ?? definition.options[0]?.value; const selectedOption = definition.options.find(opt => opt.value === currentValue); return ( setSelectMenuCords(undefined), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} > {definition.options.map((option) => ( : undefined} radii="300" onClick={() => { onChange(option.value); setSelectMenuCords(undefined); }} > {option.label} ))} } /> ); } // Color - Color Picker if (definition.type === 'color') { const currentColor = value ?? definition.default ?? '#000000'; return ( setColorMenuCords(undefined), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} > } /> ); } return null; } type PluginsProps = { requestClose: () => void; }; export function Plugins({ requestClose }: PluginsProps) { const [activeTab, setActiveTab] = useState(PluginTab.Marketplace); const [marketplacePlugins, setMarketplacePlugins] = useState([]); const [installedPlugins, setInstalledPlugins] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [expandedSettings, setExpandedSettings] = useState(null); const refreshInstalledPlugins = useCallback(async () => { try { setInstalledPlugins(await pluginMarketplaceManager.listInstalledPlugins()); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to list installed plugins'); console.error('Failed to list installed plugins:', err); } }, []); const fetchMarketplacePlugins = useCallback(async () => { setLoading(true); setError(null); try { setMarketplacePlugins(await pluginMarketplaceManager.fetchMarketplacePlugins()); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to fetch plugins'); console.error('Error fetching marketplace plugins:', err); } finally { setLoading(false); } }, []); useEffect(() => { if (activeTab === PluginTab.Marketplace) { fetchMarketplacePlugins(); } }, [activeTab, fetchMarketplacePlugins]); useEffect(() => { refreshInstalledPlugins(); }, [refreshInstalledPlugins]); const handleInstall = useCallback( async (plugin: PluginMetadata) => { try { setLoading(true); setError(null); await pluginMarketplaceManager.installPlugin(plugin); await refreshInstalledPlugins(); } catch (err) { console.error('Failed to install plugin:', err); setError(err instanceof Error ? err.message : 'Failed to install plugin'); } finally { setLoading(false); } }, [refreshInstalledPlugins] ); const handleUninstall = useCallback( async (pluginId: string) => { try { setError(null); await pluginMarketplaceManager.uninstallPlugin(pluginId); await refreshInstalledPlugins(); } catch (err) { console.error('Failed to uninstall plugin:', err); setError(err instanceof Error ? err.message : 'Failed to uninstall plugin'); } }, [refreshInstalledPlugins] ); const handleToggleEnabled = useCallback( async (pluginId: string) => { const plugin = installedPlugins.find((p) => p.id === pluginId); if (!plugin) return; const newEnabledState = !plugin.enabled; try { setError(null); await pluginMarketplaceManager.setPluginEnabled(pluginId, newEnabledState); await refreshInstalledPlugins(); if (newEnabledState) { console.log(`[Plugins] Plugin ${pluginId} enabled. Restart app to load it.`); } } catch (err) { console.error(`Failed to toggle plugin ${pluginId}:`, err); setError(err instanceof Error ? err.message : 'Failed to update plugin state'); } }, [installedPlugins, refreshInstalledPlugins] ); const isPluginInstalled = useCallback( (pluginId: string) => installedPlugins.some((p) => p.id === pluginId), [installedPlugins] ); const PluginAvatar = ({ thumbnail, name }: { thumbnail: string; name: string }) => { const [imageError, setImageError] = useState(false); return ( {!imageError ? ( {name} setImageError(true)} /> ) : ( {name.charAt(0).toUpperCase()} )} ); }; const filteredMarketplacePlugins = useMemo(() => { if (!searchQuery.trim()) return marketplacePlugins; return fuzzyFilter(marketplacePlugins, searchQuery, (plugin) => [ plugin.name, plugin.description, plugin.author, ...plugin.tags, ]).map(({ item }) => item); }, [marketplacePlugins, searchQuery]); const filteredInstalledPlugins = useMemo(() => { if (!searchQuery.trim()) return installedPlugins; return fuzzyFilter(installedPlugins, searchQuery, (plugin) => [ plugin.name, plugin.description, plugin.author, ]).map(({ item }) => item); }, [installedPlugins, searchQuery]); return (
Plugins
setActiveTab(PluginTab.Marketplace)} > Marketplace setActiveTab(PluginTab.Installed)} > Installed setSearchQuery(e.target.value)} before={} after={ searchQuery && ( setSearchQuery('')} > ) } /> {activeTab === PluginTab.Marketplace && ( {loading && ( )} {error && ( Error: {error} )} {!loading && !error && filteredMarketplacePlugins.length === 0 && ( {searchQuery ? 'No plugins found matching your search' : 'No plugins available'} )} {!loading && !error && filteredMarketplacePlugins.map((plugin) => ( { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'none'; }} > {plugin.name} by {plugin.author} v{plugin.version} {plugin.description} {plugin.tags.length > 0 && ( {plugin.tags.map((tag) => ( {tag} ))} )} ))} )} {activeTab === PluginTab.Installed && ( {filteredInstalledPlugins.length === 0 && ( {searchQuery ? 'No installed plugins found matching your search' : 'No plugins installed'} )} {filteredInstalledPlugins.map((plugin) => { const hasSettings = pluginRegistry.getPluginSettingsSchema(plugin.id) && Object.keys(pluginRegistry.getPluginSettingsSchema(plugin.id)!).length > 0; const settingsExpanded = expandedSettings === plugin.id; return ( { e.currentTarget.parentElement!.style.transform = 'translateY(-2px)'; e.currentTarget.parentElement!.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'; }} onMouseLeave={(e) => { e.currentTarget.parentElement!.style.transform = 'translateY(0)'; e.currentTarget.parentElement!.style.boxShadow = 'none'; }} > {plugin.name} by {plugin.author} v{plugin.version} {plugin.description} {plugin.enabled ? 'Enabled' : 'Disabled'} {hasSettings && ( )} {hasSettings && settingsExpanded && ( )} ); })} )}
); }