Add premium plugin detection and user guidance for manual downloads
This commit is contained in:
parent
a5e7b766ac
commit
057bba0c56
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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?;
|
||||||
|
74
src/App.css
74
src/App.css
@ -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);
|
||||||
|
}
|
||||||
|
174
src/App.tsx
174
src/App.tsx
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user