import { useState, useEffect } from "react"; import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; import { listen, UnlistenFn } from "@tauri-apps/api/event"; import { appDataDir } from '@tauri-apps/api/path'; // Import for data directory (if needed frontend side) import "./App.css"; type ServerType = | 'Paper' | 'Spigot' | 'Bukkit' | 'Vanilla' | 'Forge' | 'Fabric' | 'Velocity' | 'BungeeCord' | 'Waterfall' | 'Unknown'; interface ServerInfo { server_type: ServerType; minecraft_version?: string; plugins_directory: string; plugins_count: number; } interface Plugin { name: string; version: string; latest_version?: string; description?: string; authors: string[]; has_update: boolean; api_version?: string; main_class?: string; depend?: string[] | null; soft_depend?: string[] | null; load_before?: string[] | null; commands?: any; permissions?: any; file_path: string; file_hash: string; website?: string; changelog?: string; repository_source?: string; // Add repository source (string for simplicity) repository_id?: string; // Add repository ID repository_url?: string; // URL to the plugin's repository page } interface ScanResult { server_info: ServerInfo; plugins: Plugin[]; } interface ScanProgress { processed: number; total: number; current_file: string; } // --- New Interfaces for Update Events --- interface BulkUpdateProgressPayload { processed: number; total: number; current_plugin_name: string; } interface SingleUpdateResultPayload { original_file_path: string; plugin: Plugin | null; // Updated plugin state or null if check failed but wasn't a panic error: string | null; // Error message if any } // Interface for potential plugin matches for ambiguous plugins interface PotentialPluginMatch { name: string; version: string; repository: string; repository_id: string; page_url: string; description?: string; minecraft_versions: string[]; download_count?: number; } // --- End New Interfaces --- interface PluginDetailsProps { plugin: Plugin; onClose: () => void; } // Get server type icon function getServerTypeIcon(serverType: ServerType): string { switch (serverType) { case 'Paper': return '๐Ÿ“„'; case 'Spigot': return '๐Ÿ”Œ'; case 'Bukkit': return '๐Ÿชฃ'; case 'Vanilla': return '๐ŸงŠ'; case 'Forge': return '๐Ÿ”จ'; case 'Fabric': return '๐Ÿงต'; case 'Velocity': return 'โšก'; case 'BungeeCord': return '๐Ÿ”—'; case 'Waterfall': return '๐ŸŒŠ'; default: return 'โ“'; } } // Get a formatted server type name for display function getServerTypeName(serverType: ServerType): string { return serverType === 'Unknown' ? 'Unknown Server' : serverType; } function PluginDetails({ plugin, onClose }: PluginDetailsProps) { return (

{plugin.name}

Version: {plugin.version} {plugin.latest_version && plugin.has_update && (Update available: {plugin.latest_version})}
{plugin.description && (
{plugin.description}
)} {plugin.website && (
Website:
{plugin.website}
)} {plugin.authors && plugin.authors.length > 0 && (
Authors:
{plugin.authors.join(", ")}
)} {plugin.depend && plugin.depend.length > 0 && (
Dependencies:
{plugin.depend.join(", ")}
)} {plugin.soft_depend && plugin.soft_depend.length > 0 && (
Soft Dependencies:
{plugin.soft_depend.join(", ")}
)} {plugin.changelog && plugin.has_update && (
Changelog:
{plugin.changelog}
)}
File Path:
{plugin.file_path}
File Hash (SHA-256):
{plugin.file_hash}
{plugin.has_update && plugin.latest_version && (
)}
); } // Add this component after the PluginDetails component and before the App component function ServerInfoDisplay({ serverInfo }: { serverInfo: ServerInfo | null }) { if (!serverInfo) return null; return (

Server Information

{getServerTypeIcon(serverInfo.server_type)} {getServerTypeName(serverInfo.server_type)}
Minecraft Version {serverInfo.minecraft_version || "Unknown"}
Plugins Directory {serverInfo.plugins_directory || "Unknown"}
{serverInfo.plugins_count} plugins found
); } // Add this new component after PluginDetails function WarningModal({ message, onClose }: { message: string, onClose: () => void }) { return (

โš ๏ธ Warning

{message}

); } // Component for selecting the correct plugin from potential matches function PluginMatchSelector({ plugin, potentialMatches, onSelect, onCancel }: { plugin: Plugin, potentialMatches: PotentialPluginMatch[], onSelect: (match: PotentialPluginMatch) => void, onCancel: () => void }) { return (

Multiple Matches Found

We found several potential matches for {plugin.name}. Please select the correct one:

{potentialMatches.map((match, index) => (

{match.name}

Version: {match.version}

{match.description &&

{match.description}

}
Source: {match.repository} {match.download_count && Downloads: {match.download_count.toLocaleString()}} MC: {match.minecraft_versions.join(', ')}
View Page
))}
); } function App() { const [serverPath, setServerPath] = useState(""); const [serverInfo, setServerInfo] = useState(null); const [plugins, setPlugins] = useState([]); const [isScanning, setIsScanning] = useState(false); const [scanComplete, setScanComplete] = useState(false); const [error, setError] = useState(null); const [selectedPlugin, setSelectedPlugin] = useState(null); const [scanProgress, setScanProgress] = useState(null); const [isCheckingUpdates, setIsCheckingUpdates] = useState(false); const [updateError, setUpdateError] = useState(null); // --- New State Variables --- const [pluginLoadingStates, setPluginLoadingStates] = useState>({}); const [bulkUpdateProgress, setBulkUpdateProgress] = useState(null); const [warningMessage, setWarningMessage] = useState(null); const [potentialMatches, setPotentialMatches] = useState([]); // --- New state for match selector --- const [showMatchSelector, setShowMatchSelector] = useState(false); const [pluginToDisambiguate, setPluginToDisambiguate] = useState(null); // --- End New state for match selector --- // --- End New State Variables --- const [serverType, setServerType] = useState('Unknown'); useEffect(() => { let unlistenScanStarted: UnlistenFn | undefined; let unlistenScanProgress: UnlistenFn | undefined; let unlistenScanCompleted: UnlistenFn | undefined; let unlistenScanError: UnlistenFn | undefined; let unlistenBulkUpdateStart: UnlistenFn | undefined; let unlistenUpdateCheckProgress: UnlistenFn | undefined; let unlistenSingleUpdateCheckStarted: UnlistenFn | undefined; let unlistenSingleUpdateCheckCompleted: UnlistenFn | undefined; const setupListeners = async () => { unlistenScanStarted = await listen("scan_started", () => { console.log("Scan started event received"); setIsScanning(true); setScanProgress(null); setError(null); }); unlistenScanProgress = await listen("scan_progress", (event) => { console.log("Scan progress event received:", event.payload); setScanProgress(event.payload); }); unlistenScanCompleted = await listen("scan_completed", (event) => { console.log("Scan completed event received with payload:", event.payload); try { console.log("Server info:", event.payload.server_info); console.log("Plugins count:", event.payload.plugins.length); // Update state in a specific order to ensure UI updates properly setIsScanning(false); setScanComplete(true); setServerInfo(event.payload.server_info); setPlugins(event.payload.plugins); setServerType(event.payload.server_info.server_type); // Add a slight delay and verify the state was updated setTimeout(() => { console.log("Verifying state updates after scan:"); console.log("- scanComplete:", scanComplete); console.log("- serverInfo:", serverInfo); console.log("- plugins count:", plugins.length); // Force a state update if plugins length is still 0 but we got plugins if (plugins.length === 0 && event.payload.plugins.length > 0) { console.log("Forcing state update because plugins array is empty"); setPlugins([...event.payload.plugins]); } }, 100); console.log("State updated after scan completion"); } catch (err) { console.error("Error handling scan completion:", err); setError(`Error handling scan completion: ${err}`); setIsScanning(false); } }); unlistenScanError = await listen("scan_error", (event) => { console.log("Scan error event received:", event.payload); setIsScanning(false); setError(event.payload); }); unlistenBulkUpdateStart = await listen("bulk_update_start", (event) => { console.log("Bulk update start event received, total plugins:", event.payload); setIsCheckingUpdates(true); setUpdateError(null); setBulkUpdateProgress({ processed: 0, total: event.payload, current_plugin_name: "Starting update check..." }); }); unlistenUpdateCheckProgress = await listen("update_check_progress", (event) => { console.log("Update check progress event received:", event.payload); setBulkUpdateProgress(event.payload); }); unlistenSingleUpdateCheckStarted = await listen("single_update_check_started", (event) => { console.log("Single update check started for:", event.payload); }); unlistenSingleUpdateCheckCompleted = await listen("single_update_check_completed", (event) => { console.log("Single update check completed, result:", event.payload); const { original_file_path, plugin, error } = event.payload; setPluginLoadingStates(prev => ({ ...prev, [original_file_path]: false })); if (error) { setUpdateError(`Error checking for updates: ${error}`); return; } if (plugin) { setPlugins(prevPlugins => prevPlugins.map(p => { if (p.file_path === original_file_path) { return plugin; } return p; })); if (serverPath) { invoke("save_plugin_data", { plugins: plugins.map(p => p.file_path === original_file_path ? plugin : p), serverPath }).catch(err => { console.error("Error saving plugin data after single update:", err); }); } } }); window.addEventListener('update-plugin', ((e: CustomEvent) => { if (e.detail) { updatePlugin(e.detail); } }) as EventListener); }; setupListeners(); return () => { unlistenScanStarted?.(); unlistenScanProgress?.(); unlistenScanCompleted?.(); unlistenScanError?.(); unlistenBulkUpdateStart?.(); unlistenUpdateCheckProgress?.(); unlistenSingleUpdateCheckStarted?.(); unlistenSingleUpdateCheckCompleted?.(); window.removeEventListener('update-plugin', (() => {}) as EventListener); }; }, []); async function selectDirectory() { try { const selected = await open({ directory: true, multiple: false, title: "Select Minecraft Server Folder", }); if (selected && typeof selected === "string") { console.log(`Directory selected: ${selected}`); setServerPath(selected); setServerInfo(null); setPlugins([]); setIsScanning(false); setScanComplete(false); setError(null); setScanProgress(null); setIsCheckingUpdates(false); setUpdateError(null); setPluginLoadingStates({}); setBulkUpdateProgress(null); try { console.log(`Attempting to load persisted data for: ${selected}`); const loadedPlugins: Plugin[] = await invoke("load_plugin_data", { serverPath: selected }); if (loadedPlugins && loadedPlugins.length > 0) { console.log(`Loaded ${loadedPlugins.length} plugins from persistence.`); setPlugins(loadedPlugins); setScanComplete(true); setError(null); } else { console.log("No persisted plugin data found for this server."); } } catch (loadError) { console.error("Error loading persisted plugin data:", loadError); setError(`Failed to load previous plugin data: ${loadError}`); } } else { console.log("Directory selection cancelled."); } } catch (err) { console.error("Error selecting directory:", err); setError(`Error selecting directory: ${err}`); } } async function scanForPlugins() { if (!serverPath || isScanning) return; try { console.log("Starting scan for plugins in:", serverPath); setIsScanning(true); setScanComplete(false); setPlugins([]); setServerInfo(null); setScanProgress(null); setError(null); await invoke("scan_server_dir", { path: serverPath }); console.log("Scan server dir command invoked successfully"); } catch (err) { console.error("Error invoking scan command:", err); setError(`Failed to start scan: ${err as string}`); setIsScanning(false); } } async function checkForUpdates() { if (!plugins.length || isCheckingUpdates) return; setIsCheckingUpdates(true); setUpdateError(null); setBulkUpdateProgress(null); console.log("Invoking bulk check_plugin_updates..."); try { const repositoriesToCheck = ['SpigotMC', 'Modrinth', 'GitHub']; const pluginsToSend = plugins.map(p => ({ name: p.name, version: p.version, authors: p.authors || [], file_path: p.file_path, file_hash: p.file_hash, website: p.website, description: p.description, api_version: p.api_version, main_class: p.main_class, depend: p.depend, soft_depend: p.soft_depend, load_before: p.load_before, commands: p.commands, permissions: p.permissions, has_update: p.has_update || false, repository_source: p.repository_source, repository_id: p.repository_id, repository_url: p.repository_url, })); console.log("Sending plugin data to backend, count:", pluginsToSend.length); const updatedPlugins = await invoke("check_plugin_updates", { plugins: pluginsToSend, repositories: repositoriesToCheck, }); console.log("Bulk update check completed successfully, updating state."); setPlugins(updatedPlugins); if (serverPath) { try { console.log("[checkForUpdates] Saving plugin data..."); await invoke("save_plugin_data", { plugins: updatedPlugins, serverPath }); console.log("[checkForUpdates] Plugin data saved successfully."); } catch (saveError) { console.error("Error saving plugin data after bulk update:", saveError); setUpdateError(`Update check complete, but failed to save plugin data: ${saveError}`); } } } catch (err) { const errorMessage = `Error during bulk update check: ${err instanceof Error ? err.message : String(err)}`; console.error(errorMessage); setUpdateError(errorMessage); } finally { setIsCheckingUpdates(false); setBulkUpdateProgress(null); } } async function checkSinglePlugin(plugin: Plugin) { if (isScanning || isCheckingUpdates || pluginLoadingStates[plugin.file_path]) return; console.log(`Invoking single check for: ${plugin.name} (${plugin.file_path})`); setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: true })); setUpdateError(null); try { const repositoriesToCheck = ['SpigotMC', 'Modrinth', 'GitHub']; const pluginToSend = { name: plugin.name, version: plugin.version, authors: plugin.authors || [], file_path: plugin.file_path, file_hash: plugin.file_hash, website: plugin.website, description: plugin.description, api_version: plugin.api_version, main_class: plugin.main_class, depend: plugin.depend, soft_depend: plugin.soft_depend, load_before: plugin.load_before, commands: plugin.commands, permissions: plugin.permissions, has_update: plugin.has_update, }; await invoke("check_single_plugin_update_command", { plugin: pluginToSend, repositoriesToCheck, }); } catch (err) { const errorMessage = `Error invoking single update command for ${plugin.name}: ${err instanceof Error ? err.message : String(err)}`; console.error(errorMessage); setUpdateError(errorMessage); setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: false })); } } async function updatePlugin(plugin: Plugin) { if (!plugin.has_update || !plugin.latest_version || !plugin.repository_source || !plugin.repository_id) { setUpdateError(`Cannot update ${plugin.name}: Missing required update information`); return; } setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: true })); setUpdateError(null); try { console.log(`Updating plugin: ${plugin.name} to version ${plugin.latest_version}`); const newFilePath = await invoke("replace_plugin", { pluginId: plugin.repository_id, version: plugin.latest_version, repository: plugin.repository_source, currentFilePath: plugin.file_path, serverInfo: serverInfo }); console.log(`Update successful for ${plugin.name}, new file path: ${newFilePath}`); setPlugins(currentPlugins => currentPlugins.map(p => { if (p.file_path === plugin.file_path) { return { ...p, version: p.latest_version || p.version, has_update: false, latest_version: p.latest_version, file_path: newFilePath }; } return p; })); if (serverPath) { await invoke("save_plugin_data", { plugins: plugins.map(p => { if (p.file_path === plugin.file_path) { return { ...p, version: p.latest_version || p.version, has_update: false, file_path: newFilePath }; } return p; }), serverPath }); } } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); console.error(`Error updating ${plugin.name}:`, errorMessage); if (errorMessage.includes("in use") || errorMessage.includes("server running") || errorMessage.includes("being used by another process")) { setWarningMessage(`Cannot update ${plugin.name}: The Minecraft server appears to be running. Please stop your server before updating plugins.`); } else if (errorMessage.includes("download failed")) { setUpdateError(`Failed to download update for ${plugin.name}. Please check your internet connection and try again.`); } else if (errorMessage.includes("Critical error")) { setWarningMessage(`${errorMessage} A backup of your original plugin is available in the backups folder.`); } else { setUpdateError(`Error updating ${plugin.name}: ${errorMessage}`); } } finally { setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: false })); } } const showPluginDetails = (plugin: Plugin) => { setSelectedPlugin(plugin); }; const closePluginDetails = () => { setSelectedPlugin(null); }; const handleSelectMatch = async (selectedMatch: PotentialPluginMatch) => { if (!pluginToDisambiguate || !serverPath) return; console.log(`User selected match: ${selectedMatch.name} from ${selectedMatch.repository}`); setShowMatchSelector(false); setPluginLoadingStates(prev => ({ ...prev, [pluginToDisambiguate.file_path]: true })); try { const updatedPlugin: Plugin = await invoke("set_plugin_repository", { pluginFilePath: pluginToDisambiguate.file_path, repository: selectedMatch.repository, repositoryId: selectedMatch.repository_id, pageUrl: selectedMatch.page_url, serverPath: serverPath, }); setPlugins(currentPlugins => currentPlugins.map(p => p.file_path === updatedPlugin.file_path ? updatedPlugin : p ) ); console.log(`Successfully set repository source for ${updatedPlugin.name}`); } catch (err) { console.error("Error setting plugin repository source:", err); setUpdateError(`Failed to set repository source for ${pluginToDisambiguate.name}: ${err}`); } finally { setPluginLoadingStates(prev => ({ ...prev, [pluginToDisambiguate.file_path]: false })); setPluginToDisambiguate(null); setPotentialMatches([]); } }; const handleCancelMatchSelection = () => { setShowMatchSelector(false); setPluginToDisambiguate(null); setPotentialMatches([]); if (pluginToDisambiguate) { setPluginLoadingStates(prev => ({ ...prev, [pluginToDisambiguate.file_path]: false })); } }; return (

๐Ÿ”ง PlugSnatcher

Minecraft Plugin Manager

Select Server Directory

setServerPath(e.target.value)} placeholder="Enter server directory path..." />
{isScanning && scanProgress && (

Scanning: {scanProgress.current_file}

{scanProgress.processed} / {scanProgress.total}
)} {error && (
{error}
)}
{serverInfo && ( )} {serverInfo && plugins.length > 0 && (
{isCheckingUpdates && bulkUpdateProgress && (
Checking {bulkUpdateProgress.processed}/{bulkUpdateProgress.total}: {bulkUpdateProgress.current_plugin_name}
)} {updateError && (
{updateError}
)}
)} {plugins.length > 0 && (

Installed Plugins ({plugins.length})

Name Current Version Latest Version Actions
{plugins.map((plugin) => (
{plugin.name}
{plugin.version}
{plugin.repository_url && plugin.latest_version ? ( {plugin.latest_version} ) : ( plugin.latest_version || 'N/A' )}
{plugin.has_update && ( )}
))}
)} {scanComplete && plugins.length === 0 && (

Installed Plugins (0)

No plugins found in this directory.

)} {selectedPlugin && ( )} {warningMessage && ( setWarningMessage(null)} /> )} {showMatchSelector && pluginToDisambiguate && ( )}
); } export default App;