Add premium plugin detection and user guidance for manual downloads

This commit is contained in:
Rbanh 2025-03-30 19:55:33 -04:00
parent a5e7b766ac
commit 057bba0c56
4 changed files with 251 additions and 63 deletions

View File

@ -355,6 +355,9 @@ impl Repository for SpigotMCCrawler {
// First, get the plugin details // First, get the plugin details
let details = self.get_plugin_details(plugin_id).await?; let details = self.get_plugin_details(plugin_id).await?;
// Get the plugin page URL for potential manual download instructions
let plugin_page_url = details.page_url.clone();
// Use the SpigotMC direct download URL instead of SpiGet // Use the SpigotMC direct download URL instead of SpiGet
// SpigotMC has a direct download URL pattern for resources // SpigotMC has a direct download URL pattern for resources
let direct_download_url = format!("https://www.spigotmc.org/resources/{}/download", plugin_id); let direct_download_url = format!("https://www.spigotmc.org/resources/{}/download", plugin_id);
@ -370,19 +373,46 @@ impl Repository for SpigotMCCrawler {
Ok(destination.to_string_lossy().to_string()) Ok(destination.to_string_lossy().to_string())
}, },
Err(e) => { Err(e) => {
// If direct download fails, try the SpiGet URL as fallback // If direct download fails with 403, it might be a premium resource
println!("Direct SpigotMC download failed: {}. Trying SpiGet URL as fallback...", e); let error_message = format!("{}", e);
if error_message.contains("403 Forbidden") {
println!("Plugin appears to be a premium/protected resource on SpigotMC");
let download_url = &details.download_url; // Try the SpiGet URL as a fallback just to be sure
if download_url.is_empty() { let download_url = &details.download_url;
return Err(format!("No download URL found for SpigotMC resource {}", plugin_id)); if download_url.is_empty() {
} return Err(format!("PREMIUM_RESOURCE:{}", plugin_page_url));
}
println!("Falling back to SpiGet download URL: {}", download_url); println!("Falling back to SpiGet download URL: {}", download_url);
match self.client.download(download_url, destination).await { match self.client.download(download_url, destination).await {
Ok(_) => Ok(destination.to_string_lossy().to_string()), Ok(_) => Ok(destination.to_string_lossy().to_string()),
Err(e) => Err(format!("Failed to download from SpiGet: {}", e)) Err(spiget_err) => {
// If both methods fail, this is very likely a premium plugin
if format!("{}", spiget_err).contains("404 Not Found") {
println!("Confirmed premium resource - both direct and SpiGet downloads failed");
Err(format!("PREMIUM_RESOURCE:{}", plugin_page_url))
} else {
Err(format!("Failed to download from SpiGet: {}", spiget_err))
}
}
}
} else {
// For other errors with direct download, try SpiGet
println!("Direct SpigotMC download failed: {}. Trying SpiGet URL as fallback...", e);
let download_url = &details.download_url;
if download_url.is_empty() {
return Err(format!("No download URL found for SpigotMC resource {}", plugin_id));
}
println!("Falling back to SpiGet download URL: {}", download_url);
match self.client.download(download_url, destination).await {
Ok(_) => Ok(destination.to_string_lossy().to_string()),
Err(e) => Err(format!("Failed to download from SpiGet: {}", e))
}
} }
} }
} }

View File

@ -65,13 +65,25 @@ pub async fn replace_plugin(
// Download the new plugin version // Download the new plugin version
let server_type = server_info.as_ref().map(|info| &info.server_type); let server_type = server_info.as_ref().map(|info| &info.server_type);
crate::lib_download_plugin_from_repository( let download_result = crate::lib_download_plugin_from_repository(
&plugin_id, &plugin_id,
&version, &version,
repository, repository,
&download_path.to_string_lossy(), &download_path.to_string_lossy(),
server_type server_type
).await?; ).await;
// Check for premium resource indicator
if let Err(error) = &download_result {
if error.starts_with("PREMIUM_RESOURCE:") {
// Extract the resource URL from the error
let resource_url = error.strip_prefix("PREMIUM_RESOURCE:").unwrap_or_default();
return Err(format!("PREMIUM_RESOURCE:{}:{}:{}", plugin_id, version, resource_url));
}
}
// If other error, propagate it
let _ = download_result?;
// Backup the original file // Backup the original file
backup_plugin(current_file_path.clone()).await?; backup_plugin(current_file_path.clone()).await?;

View File

@ -642,3 +642,77 @@ button {
.warning-content .close-button:hover { .warning-content .close-button:hover {
background-color: #e69500; background-color: #e69500;
} }
/* Premium Plugin Modal Styles */
.premium-plugin-modal {
background-color: var(--surface-color);
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
width: 90%;
max-width: 600px;
position: relative;
color: var(--text-color);
border: 1px solid var(--border-color);
}
.premium-plugin-modal h3 {
color: var(--warning-color, #ff9800);
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.4rem;
}
.premium-plugin-instructions {
margin: 1.2rem 0;
background-color: rgba(255, 255, 255, 0.05);
padding: 1rem;
border-radius: 4px;
border-left: 3px solid var(--accent-color);
}
.premium-plugin-instructions ol {
margin-top: 0.5rem;
padding-left: 1.5rem;
}
.premium-plugin-instructions li {
margin-bottom: 0.5rem;
}
.premium-plugin-actions {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
.plugin-page-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: var(--accent-color);
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: bold;
transition: background-color 0.2s;
}
.plugin-page-button:hover {
background-color: var(--accent-hover-color, #1967d2);
}
.close-modal-button {
padding: 0.75rem 1.5rem;
background-color: var(--surface-alt-color, #424242);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
.close-modal-button:hover {
background-color: var(--surface-hover-color, #616161);
}

View File

@ -293,6 +293,53 @@ function PluginMatchSelector({
); );
} }
// After the WarningModal component, add this new component
function PremiumPluginModal({
pluginName,
pluginVersion,
resourceUrl,
onClose
}: {
pluginName: string;
pluginVersion: string;
resourceUrl: string;
onClose: () => void;
}) {
return (
<div className="modal-backdrop">
<div className="premium-plugin-modal">
<h3>Premium Plugin Detected</h3>
<p>
<strong>{pluginName} {pluginVersion}</strong> appears to be a premium or protected plugin
that requires manual download.
</p>
<div className="premium-plugin-instructions">
<p>To update this plugin:</p>
<ol>
<li>Visit the plugin page on SpigotMC</li>
<li>Log in to your SpigotMC account</li>
<li>Download the latest version manually</li>
<li>Replace the current plugin file in your server's plugins folder</li>
</ol>
</div>
<div className="premium-plugin-actions">
<a
href={resourceUrl}
target="_blank"
rel="noopener noreferrer"
className="plugin-page-button"
>
Open Plugin Page
</a>
<button className="close-modal-button" onClick={onClose}>
Close
</button>
</div>
</div>
</div>
);
}
function App() { function App() {
const [serverPath, setServerPath] = useState(""); const [serverPath, setServerPath] = useState("");
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null); const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
@ -315,6 +362,7 @@ function App() {
// --- End New state for match selector --- // --- End New state for match selector ---
// --- End New State Variables --- // --- End New State Variables ---
const [serverType, setServerType] = useState<ServerType>('Unknown'); const [serverType, setServerType] = useState<ServerType>('Unknown');
const [premiumPluginInfo, setPremiumPluginInfo] = useState<{name: string; version: string; url: string} | null>(null);
useEffect(() => { useEffect(() => {
let unlistenScanStarted: UnlistenFn | undefined; let unlistenScanStarted: UnlistenFn | undefined;
@ -634,60 +682,75 @@ function App() {
try { try {
console.log(`Updating plugin: ${plugin.name} to version ${plugin.latest_version}`); console.log(`Updating plugin: ${plugin.name} to version ${plugin.latest_version}`);
const newFilePath = await invoke<string>("update_plugin", { // Try to update the plugin
pluginId: plugin.repository_id, try {
version: plugin.latest_version, const newFilePath = await invoke<string>("update_plugin", {
repository: plugin.repository_source, pluginId: plugin.repository_id,
currentFilePath: plugin.file_path, version: plugin.latest_version,
serverTypeStr: serverInfo?.server_type repository: plugin.repository_source,
}); currentFilePath: plugin.file_path,
serverTypeStr: serverInfo?.server_type
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") || console.log(`Update successful for ${plugin.name}, new file path: ${newFilePath}`);
errorMessage.includes("server running") ||
errorMessage.includes("being used by another process")) { setPlugins(currentPlugins => currentPlugins.map(p => {
setWarningMessage(`Cannot update ${plugin.name}: The Minecraft server appears to be running. Please stop your server before updating plugins.`); if (p.file_path === plugin.file_path) {
} else if (errorMessage.includes("download failed")) { return {
setUpdateError(`Failed to download update for ${plugin.name}. Please check your internet connection and try again.`); ...p,
} else if (errorMessage.includes("Critical error")) { version: p.latest_version || p.version,
setWarningMessage(`${errorMessage} A backup of your original plugin is available in the backups folder.`); has_update: false,
} else { latest_version: p.latest_version,
setUpdateError(`Error updating ${plugin.name}: ${errorMessage}`); 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 (error: any) {
console.error(`Error updating plugin: ${error}`);
// Check if this is a premium resource
const errorMsg = String(error);
if (errorMsg.startsWith("PREMIUM_RESOURCE:")) {
// Parse the premium resource error
const parts = errorMsg.split(":");
if (parts.length >= 4) {
const resourceUrl = parts.slice(3).join(":"); // Rejoin in case URL contains colons
// Show premium resource modal
setPremiumPluginInfo({
name: plugin.name,
version: plugin.latest_version,
url: resourceUrl
});
} else {
setUpdateError(`Premium plugin detected, but couldn't get resource URL: ${plugin.name}`);
}
} else {
// Standard error handling
setUpdateError(`Failed to update ${plugin.name}: ${error}`);
}
} }
} catch (error) {
console.error(`Error in updatePlugin: ${error}`);
setUpdateError(`Update process error: ${error}`);
} finally { } finally {
setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: false })); setPluginLoadingStates(prev => ({ ...prev, [plugin.file_path]: false }));
} }
@ -885,6 +948,15 @@ function App() {
/> />
)} )}
{premiumPluginInfo && (
<PremiumPluginModal
pluginName={premiumPluginInfo.name}
pluginVersion={premiumPluginInfo.version}
resourceUrl={premiumPluginInfo.url}
onClose={() => setPremiumPluginInfo(null)}
/>
)}
</main> </main>
<footer className="app-footer"> <footer className="app-footer">