504 lines
16 KiB
TypeScript
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; |