import React, { createContext, useState, useCallback, ReactNode, useEffect } from 'react'; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { Plugin, PotentialPluginMatch } from '../../types/plugin.types'; import { BulkUpdateProgressPayload, SingleUpdateResultPayload } from '../../types/events.types'; import { ScanResult } from '../../types/server.types'; import { canUpdatePlugin } from '../../utils/validators'; import { createUpdateMessage } from '../../utils/formatters'; import { ServerType } from '../../types/server.types'; import { useServerContext } from '../ServerContext/useServerContext'; interface PluginContextProps { /** * List of plugins installed on the server */ plugins: Plugin[]; /** * Currently selected plugin (for details view) */ selectedPlugin: Plugin | null; /** * Whether plugin updates are being checked */ isCheckingUpdates: boolean; /** * Error message specific to plugin operations */ updateError: string | null; /** * Loading states for individual plugins (keyed by file_path) */ pluginLoadingStates: Record; /** * Progress information for bulk update checks */ bulkUpdateProgress: BulkUpdateProgressPayload | null; /** * Whether a single plugin update check is in progress */ isCheckingSinglePlugin: boolean; /** * Function to check for updates for all plugins */ checkForUpdates: (serverType?: ServerType) => Promise; /** * Function to check for updates for a single plugin */ checkSinglePlugin: (plugin: Plugin) => Promise; /** * Function to update a plugin to the latest version */ updatePlugin: (plugin: Plugin) => Promise; /** * Function to select a plugin for viewing details */ showPluginDetails: (plugin: Plugin) => void; /** * Function to close the plugin details view */ closePluginDetails: () => void; /** * Function to set the plugins array directly */ setPlugins: (plugins: Plugin[]) => void; /** * Function to clear update errors */ clearUpdateError: () => void; } // Create the context with default values export const PluginContext = createContext({} as PluginContextProps); interface PluginProviderProps { children: ReactNode; } /** * Provider component for managing plugin-related state */ export const PluginProvider: React.FC = ({ children }) => { // Get server context directly const { serverPath, serverInfo } = useServerContext(); const serverType = serverInfo?.server_type; const [plugins, setPluginsState] = useState([]); const [selectedPlugin, setSelectedPlugin] = useState(null); const [isCheckingUpdates, setIsCheckingUpdates] = useState(false); const [updateError, setUpdateError] = useState(null); const [pluginLoadingStates, setPluginLoadingStates] = useState>({}); const [bulkUpdateProgress, setBulkUpdateProgress] = useState(null); const [isCheckingSinglePlugin, setIsCheckingSinglePlugin] = useState(false); // Setup event listeners useEffect(() => { const unlisteners: (() => void)[] = []; // Listen for scan-completed events to update plugins listen('scan_completed', (event) => { console.log("Received scan_completed event in PluginContext:", event.payload); const result = event.payload as ScanResult; // Update the plugins state with the scanned plugins setPluginsState(result.plugins); console.log(`Updated plugins state with ${result.plugins.length} plugins`); }).then(unlisten => unlisteners.push(unlisten)); // Listen for update check progress listen("update_check_progress", (event) => { console.log("Update check progress event received:", event.payload); setBulkUpdateProgress(event.payload); }).then(unlisten => unlisteners.push(unlisten)); // Listen for single update check completed listen("single_update_check_completed", (eventData) => { const { original_file_path, plugin: updatedPlugin, error } = eventData.payload; console.log("Single update check completed event received:", original_file_path); if (error) { console.error("Error checking plugin for updates:", error); setUpdateError(error); } if (updatedPlugin) { console.log("Plugin update check result:", updatedPlugin); // Update the plugin in the list setPluginsState(currentPlugins => { return currentPlugins.map(p => { if (p.file_path === original_file_path) { return updatedPlugin; } return p; }); }); } // Clear loading state for this plugin - IMPORTANT: always clear the loading state console.log(`Clearing loading state for plugin path: ${original_file_path}`); setPluginLoadingStates(prev => { const newState = { ...prev }; delete newState[original_file_path]; return newState; }); // Also notify the UI that the check is complete const customEvent = new CustomEvent('single_plugin_check_completed', { detail: { plugin_path: original_file_path, success: !error, error: error } }); window.dispatchEvent(customEvent); }).then(unlisten => unlisteners.push(unlisten)); // Listen for update check complete listen("update_check_complete", (event) => { console.log("Update check complete event received:", event); // Optionally handle any additional logic needed when bulk update is complete }).then(unlisten => unlisteners.push(unlisten)); // Cleanup function return () => { unlisteners.forEach(unlisten => unlisten()); }; }, []); // Set plugins directly const setPlugins = useCallback((newPlugins: Plugin[]) => { setPluginsState(newPlugins); }, []); // Clear update error const clearUpdateError = useCallback(() => { setUpdateError(null); }, []); // Log when serverPath changes useEffect(() => { console.log(`PluginContext: serverPath changed to ${serverPath}`); }, [serverPath]); // Check for updates for all plugins const checkForUpdates = useCallback(async (currentServerType?: ServerType) => { const currentServerPath = serverPath; console.log(`checkForUpdates called with serverPath: ${currentServerPath}`); if (!plugins.length) { console.error('No plugins to check for updates'); setUpdateError('No plugins to check for updates'); return; } if (isCheckingUpdates) { console.warn('Update check already in progress'); return; } if (!currentServerPath) { console.error('No server path available in PluginContext'); setUpdateError('No server path available'); return; } console.log(`Starting update check with serverPath: ${currentServerPath}`); console.log(`Total plugins to check: ${plugins.length}`); console.log(`Server type: ${currentServerType || serverType || 'Unknown'}`); setIsCheckingUpdates(true); setUpdateError(null); setBulkUpdateProgress(null); console.log("Invoking bulk check_plugin_updates..."); try { // Include all repositories to check const repositoriesToCheck = ['hangarmc', 'spigotmc', 'modrinth', 'github']; // Prepare plugins data with correct structure 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); console.log("Using repositories:", repositoriesToCheck); console.log("Sample plugin data:", pluginsToSend[0]); const updatedPlugins = await invoke("check_plugin_updates", { plugins: pluginsToSend, repositories: repositoriesToCheck, }); console.log("Bulk update check completed successfully, updating state."); console.log(`Received ${updatedPlugins.length} updated plugins from backend`); // Ensure up-to-date plugins have a latest_version const processedPlugins = updatedPlugins.map(plugin => { if (!plugin.latest_version && !plugin.has_update) { return { ...plugin, latest_version: plugin.version }; } return plugin; }); setPlugins(processedPlugins); // Emit an update check complete event if not emitted by backend let updatedCount = processedPlugins.filter(p => p.has_update).length; console.log(`Update check complete: ${updatedCount} plugins need updates`); // Send a custom update_check_complete event to ensure the UI is updated const event = new CustomEvent('update_check_complete', { detail: { outdated_count: updatedCount, total_checked: processedPlugins.length, success: true } }); window.dispatchEvent(event); if (currentServerPath) { try { console.log("[checkForUpdates] Saving plugin data..."); await invoke("save_plugin_data", { plugins: processedPlugins, serverPath: currentServerPath }); 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 (error) { console.error("Error checking for updates:", error); setUpdateError(`Failed to check for updates: ${error}`); } finally { setIsCheckingUpdates(false); setBulkUpdateProgress(null); } }, [plugins, isCheckingUpdates, serverPath, setPlugins, serverType]); // Check for updates for a single plugin const checkSinglePlugin = useCallback(async (plugin: Plugin) => { const currentServerPath = serverPath; console.log(`checkSinglePlugin called with serverPath: ${currentServerPath}`); if (!currentServerPath) { console.error('No server path available for checking single plugin'); setUpdateError('No server path available'); return; } setIsCheckingSinglePlugin(true); setUpdateError(null); setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: true })); try { console.log(`Checking for updates for plugin: ${plugin.name}`); // Use all lowercase repository names to match backend expectations const repositories = ["hangarmc", "spigotmc", "modrinth", "github"]; console.log(`Repositories to check: ${repositories.join(', ')}`); await invoke("check_single_plugin_update_command", { plugin: plugin, repositories: repositories }); console.log(`Single plugin update check initiated for ${plugin.name}`); } catch (err) { console.error(`Error checking plugin ${plugin.name} for updates:`, err); setUpdateError(`Failed to check ${plugin.name} for updates: ${err}`); // Clear loading state for this plugin setPluginLoadingStates(prev => { const newState = { ...prev }; delete newState[plugin.file_path]; return newState; }); } finally { setIsCheckingSinglePlugin(false); } }, [serverPath]); // Update a plugin to the latest version const updatePlugin = useCallback(async (plugin: Plugin) => { const currentServerPath = serverPath; const currentServerType = serverType; console.log(`updatePlugin called with serverPath: ${currentServerPath}`); if (!canUpdatePlugin(plugin)) { setUpdateError(`Cannot update ${plugin.name}: Missing required update information`); return; } if (!currentServerPath) { console.error('No server path available for updating plugin'); setUpdateError('No server path available'); return; } // Set loading state for this plugin setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: true })); setUpdateError(null); try { console.log(`Updating plugin: ${plugin.name} to version ${plugin.latest_version}`); // Create update message const updateMessage = createUpdateMessage( plugin.name, plugin.latest_version, plugin.platform_compatibility ); // This is simplified as the actual UI feedback would be handled by the UIContext console.log(updateMessage); const newFilePath = await invoke("update_plugin", { pluginId: plugin.repository_id, version: plugin.latest_version, repository: plugin.repository_source, currentFilePath: plugin.file_path, serverTypeStr: currentServerType }); console.log(`Update successful for ${plugin.name}, new file path: ${newFilePath}`); // Update the plugins array with the updated plugin const updatedPlugins = plugins.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; }); setPluginsState(updatedPlugins); // Save updated plugins data if (currentServerPath) { try { await invoke("save_plugin_data", { plugins: updatedPlugins, serverPath: currentServerPath }); console.log(`Plugin data saved successfully after updating ${plugin.name}`); } catch (saveError) { console.error(`Error saving plugin data after update: ${saveError}`); setUpdateError(`Plugin updated, but failed to save plugin data: ${saveError}`); } } // Dispatch custom event for plugin update completed const event = new CustomEvent('single_plugin_updated', { detail: { plugin_name: plugin.name, old_path: plugin.file_path, new_path: newFilePath, success: true } }); window.dispatchEvent(event); } catch (error) { console.error(`Error updating plugin: ${error}`); setUpdateError(`Failed to update ${plugin.name}: ${error}`); // Dispatch custom event for plugin update failed const event = new CustomEvent('single_plugin_update_failed', { detail: { plugin_name: plugin.name, error: String(error) } }); window.dispatchEvent(event); } finally { // Clear loading state for this plugin setPluginLoadingStates(prev => { const newState = { ...prev }; delete newState[plugin.file_path]; return newState; }); } }, [plugins, serverPath, serverType]); // Show plugin details const showPluginDetails = useCallback((plugin: Plugin) => { setSelectedPlugin(plugin); }, []); // Close plugin details const closePluginDetails = useCallback(() => { setSelectedPlugin(null); }, []); // Define the context value const contextValue: PluginContextProps = { plugins, selectedPlugin, isCheckingUpdates, updateError, pluginLoadingStates, bulkUpdateProgress, isCheckingSinglePlugin, checkForUpdates, checkSinglePlugin, updatePlugin, showPluginDetails, closePluginDetails, setPlugins, clearUpdateError }; return ( {children} ); }; export default PluginProvider;