PlugSnatcher/src/context/PluginContext/PluginContext.tsx

504 lines
16 KiB
TypeScript

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<string, boolean>;
/**
* 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<void>;
/**
* Function to check for updates for a single plugin
*/
checkSinglePlugin: (plugin: Plugin) => Promise<void>;
/**
* Function to update a plugin to the latest version
*/
updatePlugin: (plugin: Plugin) => Promise<void>;
/**
* 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<PluginContextProps>({} as PluginContextProps);
interface PluginProviderProps {
children: ReactNode;
}
/**
* Provider component for managing plugin-related state
*/
export const PluginProvider: React.FC<PluginProviderProps> = ({
children
}) => {
// Get server context directly
const { serverPath, serverInfo } = useServerContext();
const serverType = serverInfo?.server_type;
const [plugins, setPluginsState] = useState<Plugin[]>([]);
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null);
const [isCheckingUpdates, setIsCheckingUpdates] = useState<boolean>(false);
const [updateError, setUpdateError] = useState<string | null>(null);
const [pluginLoadingStates, setPluginLoadingStates] = useState<Record<string, boolean>>({});
const [bulkUpdateProgress, setBulkUpdateProgress] = useState<BulkUpdateProgressPayload | null>(null);
const [isCheckingSinglePlugin, setIsCheckingSinglePlugin] = useState<boolean>(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<BulkUpdateProgressPayload>("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<SingleUpdateResultPayload>("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<Plugin[]>("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<string>("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 (
<PluginContext.Provider value={contextValue}>
{children}
</PluginContext.Provider>
);
};
export default PluginProvider;