mirror of
https://gitlab.com/2009scape/rt4-client.git
synced 2025-12-10 10:20:44 -07:00
remote downloading
This commit is contained in:
parent
905393d2be
commit
dc70d4406e
3 changed files with 836 additions and 26 deletions
|
|
@ -0,0 +1,413 @@
|
|||
package KondoKit.components.ReflectiveEditorComponents
|
||||
|
||||
import KondoKit.views.ReflectiveEditorView
|
||||
import plugin.PluginRepository
|
||||
import java.awt.EventQueue
|
||||
import java.io.*
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
/**
|
||||
* Manages downloading and installing plugins from GitLab with concurrent support
|
||||
*/
|
||||
object PluginDownloadManager {
|
||||
private const val GITLAB_PROJECT_PATH = "2009scape/tools/client-plugins"
|
||||
private const val GITLAB_BRANCH = "master"
|
||||
private const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
private const val MAX_CONCURRENT_DOWNLOADS = 3
|
||||
private const val DEBUG = true // Set to false to disable debug logging
|
||||
|
||||
// Thread pool for concurrent downloads
|
||||
private val downloadExecutor = Executors.newFixedThreadPool(MAX_CONCURRENT_DOWNLOADS)
|
||||
|
||||
// Callback for download progress updates
|
||||
interface DownloadProgressCallback {
|
||||
fun onProgress(pluginName: String, progress: Int)
|
||||
fun onComplete(pluginName: String, success: Boolean, errorMessage: String? = null)
|
||||
}
|
||||
|
||||
// Debug logging function
|
||||
private fun debugLog(message: String) {
|
||||
if (DEBUG) {
|
||||
println("[PluginDownloadManager] $message")
|
||||
}
|
||||
}
|
||||
|
||||
// Test URL method for debugging
|
||||
fun testDownloadUrl(plugin: GitLabPlugin): String {
|
||||
return "https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a single plugin
|
||||
*/
|
||||
fun downloadPlugin(plugin: GitLabPlugin, callback: (String, Boolean, String?) -> Unit) {
|
||||
downloadExecutor.submit {
|
||||
try {
|
||||
debugLog("Starting download for plugin: ${plugin.path}")
|
||||
|
||||
// Download the plugin as a ZIP archive
|
||||
val success = downloadAndExtractPlugin(plugin, object : DownloadProgressCallback {
|
||||
override fun onProgress(pluginName: String, progress: Int) {
|
||||
// We don't need to do anything here for the simple callback
|
||||
}
|
||||
|
||||
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
|
||||
callback(pluginName, success, errorMessage)
|
||||
}
|
||||
})
|
||||
|
||||
if (success) {
|
||||
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback(plugin.path, true, null)
|
||||
}
|
||||
} else {
|
||||
debugLog("Failed to download plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback(plugin.path, false, "Failed to download plugin")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during download of ${plugin.path}: ${e.message}")
|
||||
e.printStackTrace()
|
||||
SwingUtilities.invokeLater {
|
||||
callback(plugin.path, false, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a single plugin with progress updates
|
||||
*/
|
||||
fun downloadPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback) {
|
||||
downloadExecutor.submit {
|
||||
try {
|
||||
debugLog("Starting download for plugin: ${plugin.path}")
|
||||
callback.onProgress(plugin.path, 0)
|
||||
|
||||
// Download the plugin as a ZIP archive
|
||||
val success = downloadAndExtractPlugin(plugin, callback)
|
||||
|
||||
if (success) {
|
||||
debugLog("Successfully downloaded and extracted plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onComplete(plugin.path, true)
|
||||
}
|
||||
} else {
|
||||
debugLog("Failed to download plugin: ${plugin.path}")
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onComplete(plugin.path, false, "Failed to download plugin")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during download of ${plugin.path}: ${e.message}")
|
||||
e.printStackTrace()
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onComplete(plugin.path, false, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download multiple plugins concurrently
|
||||
*/
|
||||
fun downloadPlugins(plugins: List<GitLabPlugin>, callback: (String, Boolean, String?) -> Unit) {
|
||||
debugLog("Starting concurrent download of ${plugins.size} plugins")
|
||||
|
||||
// Submit all downloads to the executor
|
||||
for (plugin in plugins) {
|
||||
downloadPlugin(plugin, object : DownloadProgressCallback {
|
||||
override fun onProgress(pluginName: String, progress: Int) {
|
||||
// We don't need to do anything here for the simple callback
|
||||
}
|
||||
|
||||
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
|
||||
callback(pluginName, success, errorMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and extract a plugin from GitLab
|
||||
*/
|
||||
private fun downloadAndExtractPlugin(plugin: GitLabPlugin, callback: DownloadProgressCallback): Boolean {
|
||||
try {
|
||||
// Validate plugin path
|
||||
if (plugin.path.isBlank()) {
|
||||
debugLog("Plugin path is blank for plugin: ${plugin.path}")
|
||||
return false
|
||||
}
|
||||
|
||||
debugLog("Starting download for plugin: ${plugin.path}")
|
||||
|
||||
// Try multiple URL formats since GitLab has changed their API
|
||||
val downloadUrls = listOf(
|
||||
// Format 1: Using ref_type parameter
|
||||
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}&ref_type=heads",
|
||||
// Format 2: Using ref parameter
|
||||
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip?path=${plugin.path}",
|
||||
// Format 3: Direct archive URL without path parameter (we'll filter later)
|
||||
"https://gitlab.com/$GITLAB_PROJECT_PATH/-/archive/$GITLAB_BRANCH/${GITLAB_PROJECT_PATH.replace("/", "-")}-$GITLAB_BRANCH.zip"
|
||||
)
|
||||
|
||||
for (downloadUrl in downloadUrls) {
|
||||
debugLog("Trying download URL: $downloadUrl")
|
||||
|
||||
// Validate URL
|
||||
val url = try {
|
||||
URL(downloadUrl)
|
||||
} catch (e: Exception) {
|
||||
debugLog("Invalid URL: $downloadUrl for plugin: ${plugin.path}. Error: ${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
// Create URL connection
|
||||
val connection = url.openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.setRequestProperty("User-Agent", CHROME_USER_AGENT)
|
||||
// Add Accept header to avoid 406 errors
|
||||
connection.setRequestProperty("Accept", "*/*")
|
||||
|
||||
debugLog("Connection opened for plugin: ${plugin.path}")
|
||||
debugLog("Request headers - User-Agent: ${connection.getRequestProperty("User-Agent")}")
|
||||
debugLog("Request headers - Accept: ${connection.getRequestProperty("Accept")}")
|
||||
|
||||
// Get content length for progress tracking
|
||||
val contentLength = connection.contentLength
|
||||
debugLog("Content length: $contentLength bytes for plugin: ${plugin.path}")
|
||||
|
||||
// Read response
|
||||
val responseCode = connection.responseCode
|
||||
debugLog("Response code: $responseCode for plugin: ${plugin.path}")
|
||||
|
||||
// Log response message
|
||||
val responseMessage = connection.responseMessage
|
||||
debugLog("Response message: $responseMessage for plugin: ${plugin.path}")
|
||||
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
// Check if input stream is available
|
||||
val inputStream = connection.inputStream
|
||||
if (inputStream == null) {
|
||||
debugLog("Input stream is null for plugin: ${plugin.path}")
|
||||
continue
|
||||
}
|
||||
|
||||
debugLog("Input stream available for plugin: ${plugin.path}")
|
||||
|
||||
// Create output directory
|
||||
val pluginsDir = ReflectiveEditorView.pluginsDirectory
|
||||
debugLog("Plugins directory: ${pluginsDir.absolutePath}")
|
||||
|
||||
// Validate plugins directory
|
||||
if (!pluginsDir.exists()) {
|
||||
debugLog("Plugins directory does not exist: ${pluginsDir.absolutePath}")
|
||||
if (!pluginsDir.mkdirs()) {
|
||||
debugLog("Failed to create plugins directory: ${pluginsDir.absolutePath}")
|
||||
return false
|
||||
}
|
||||
debugLog("Created plugins directory: ${pluginsDir.absolutePath}")
|
||||
}
|
||||
|
||||
if (!pluginsDir.isDirectory) {
|
||||
debugLog("Plugins path is not a directory: ${pluginsDir.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
val pluginDir = File(pluginsDir, plugin.path)
|
||||
debugLog("Plugin directory: ${pluginDir.absolutePath}")
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!pluginDir.exists()) {
|
||||
pluginDir.mkdirs()
|
||||
debugLog("Created plugin directory: ${pluginDir.absolutePath}")
|
||||
}
|
||||
|
||||
// Download the ZIP file
|
||||
val tempZipFile = File.createTempFile("plugin_", ".zip")
|
||||
tempZipFile.deleteOnExit()
|
||||
debugLog("Created temp file: ${tempZipFile.absolutePath}")
|
||||
|
||||
var totalBytesRead = 0L
|
||||
inputStream.use { input ->
|
||||
FileOutputStream(tempZipFile).use { outputStream ->
|
||||
val buffer = ByteArray(8192)
|
||||
var bytesRead: Int
|
||||
|
||||
debugLog("Starting to read data for plugin: ${plugin.path}")
|
||||
|
||||
while (input.read(buffer).also { bytesRead = it } != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
totalBytesRead += bytesRead
|
||||
|
||||
// Report progress if content length is known
|
||||
if (contentLength > 0) {
|
||||
val progress = (totalBytesRead * 100 / contentLength).toInt()
|
||||
// Limit progress updates to avoid flooding the UI
|
||||
if (progress % 5 == 0) {
|
||||
SwingUtilities.invokeLater {
|
||||
callback.onProgress(plugin.path, progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("Finished reading data for plugin: ${plugin.path}. Total bytes: $totalBytesRead")
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("Downloaded ${totalBytesRead} bytes to ${tempZipFile.absolutePath}")
|
||||
|
||||
// Extract the ZIP file
|
||||
if (extractZipFile(tempZipFile, pluginDir, plugin.path)) {
|
||||
debugLog("Successfully extracted plugin to ${pluginDir.absolutePath}")
|
||||
// Clean up temp file
|
||||
tempZipFile.delete()
|
||||
return true
|
||||
} else {
|
||||
debugLog("Failed to extract plugin")
|
||||
tempZipFile.delete()
|
||||
continue // Try next URL format
|
||||
}
|
||||
} else {
|
||||
debugLog("HTTP error: $responseCode for plugin: ${plugin.path} with URL: $downloadUrl")
|
||||
|
||||
// Try to read error stream for more details
|
||||
try {
|
||||
val errorStream = connection.errorStream
|
||||
if (errorStream != null) {
|
||||
val errorReader = BufferedReader(InputStreamReader(errorStream))
|
||||
val errorResponse = errorReader.use { it.readText() }
|
||||
debugLog("Error response: $errorResponse for plugin: ${plugin.path}")
|
||||
} else {
|
||||
debugLog("Error stream is null for plugin: ${plugin.path}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception while reading error stream: ${e.message} for plugin: ${plugin.path}")
|
||||
}
|
||||
|
||||
continue // Try next URL format
|
||||
}
|
||||
}
|
||||
|
||||
debugLog("All URL formats failed for plugin: ${plugin.path}")
|
||||
return false
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during download and extraction for plugin ${plugin.path}: ${e.message}")
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a ZIP file to the specified directory
|
||||
*/
|
||||
private fun extractZipFile(zipFile: File, targetDir: File, pluginPath: String): Boolean {
|
||||
try {
|
||||
debugLog("Extracting ZIP file: ${zipFile.absolutePath} to ${targetDir.absolutePath}")
|
||||
|
||||
// Validate inputs
|
||||
if (!zipFile.exists()) {
|
||||
debugLog("ZIP file does not exist: ${zipFile.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!targetDir.exists() && !targetDir.mkdirs()) {
|
||||
debugLog("Failed to create target directory: ${targetDir.absolutePath}")
|
||||
return false
|
||||
}
|
||||
|
||||
ZipInputStream(FileInputStream(zipFile)).use { zis ->
|
||||
var entry: ZipEntry?
|
||||
var entryCount = 0
|
||||
|
||||
while (zis.nextEntry.also { entry = it } != null) {
|
||||
entryCount++
|
||||
val entryName = entry!!.name
|
||||
|
||||
debugLog("Processing ZIP entry $entryCount: $entryName")
|
||||
|
||||
// Skip the top-level directory in the ZIP (GitLab adds a project-branch-hash directory)
|
||||
// We want to extract the contents of the plugin directory directly
|
||||
val relativePath = if (entryName.contains("/")) {
|
||||
entryName.substring(entryName.indexOf("/") + 1)
|
||||
} else {
|
||||
entryName
|
||||
}
|
||||
|
||||
debugLog("Relative path for entry: $relativePath")
|
||||
|
||||
// Only extract files that are part of this specific plugin
|
||||
if (relativePath.startsWith(pluginPath) && relativePath != pluginPath) {
|
||||
val fileName = relativePath.substring(pluginPath.length + 1) // +1 for the trailing slash
|
||||
|
||||
debugLog("File name to extract: $fileName")
|
||||
|
||||
if (fileName.isNotEmpty()) {
|
||||
val file = File(targetDir, fileName)
|
||||
|
||||
debugLog("Target file path: ${file.absolutePath}")
|
||||
|
||||
// Create parent directories if needed
|
||||
val parent = file.parentFile
|
||||
if (parent != null && !parent.exists()) {
|
||||
if (parent.mkdirs()) {
|
||||
debugLog("Created parent directories: ${parent.absolutePath}")
|
||||
} else {
|
||||
debugLog("Failed to create parent directories: ${parent.absolutePath}")
|
||||
zis.closeEntry()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Extract file or directory
|
||||
if (entry!!.isDirectory) {
|
||||
if (!file.exists()) {
|
||||
if (file.mkdirs()) {
|
||||
debugLog("Created directory: ${file.absolutePath}")
|
||||
} else {
|
||||
debugLog("Failed to create directory: ${file.absolutePath}")
|
||||
}
|
||||
} else {
|
||||
debugLog("Directory already exists: ${file.absolutePath}")
|
||||
}
|
||||
} else {
|
||||
// Create file
|
||||
try {
|
||||
FileOutputStream(file).use { fos ->
|
||||
zis.copyTo(fos)
|
||||
}
|
||||
debugLog("Extracted file: ${file.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
debugLog("Failed to extract file ${file.absolutePath}: ${e.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugLog("Skipping empty file name")
|
||||
}
|
||||
} else {
|
||||
debugLog("Skipping entry (not part of plugin): $relativePath")
|
||||
}
|
||||
|
||||
zis.closeEntry()
|
||||
}
|
||||
|
||||
debugLog("Processed $entryCount entries from ZIP file")
|
||||
}
|
||||
|
||||
debugLog("Successfully extracted ZIP file")
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
debugLog("Exception during ZIP extraction: ${e.message}")
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,5 +11,7 @@ data class PluginStatus(
|
|||
val author: String?,
|
||||
val gitLabPlugin: GitLabPlugin?,
|
||||
val isInstalled: Boolean,
|
||||
val needsUpdate: Boolean
|
||||
val needsUpdate: Boolean,
|
||||
val isDownloading: Boolean = false,
|
||||
val downloadProgress: Int = 0
|
||||
)
|
||||
|
|
@ -5,6 +5,8 @@ import KondoKit.components.*
|
|||
import KondoKit.components.ReflectiveEditorComponents.CustomSearchField
|
||||
import KondoKit.components.ReflectiveEditorComponents.GitLabPlugin
|
||||
import KondoKit.components.ReflectiveEditorComponents.GitLabPluginFetcher
|
||||
import KondoKit.components.ReflectiveEditorComponents.PluginDownloadManager
|
||||
import KondoKit.components.ReflectiveEditorComponents.PluginProperties
|
||||
import KondoKit.components.ReflectiveEditorComponents.PluginStatus
|
||||
import KondoKit.Helpers.showToast
|
||||
import KondoKit.plugin
|
||||
|
|
@ -19,9 +21,12 @@ import plugin.PluginInfo
|
|||
import plugin.PluginRepository
|
||||
import rt4.GlobalJsonConfig
|
||||
import java.awt.*
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.imageio.ImageIO
|
||||
import javax.swing.*
|
||||
import kotlin.math.ceil
|
||||
|
|
@ -268,6 +273,22 @@ object ReflectiveEditorView : View {
|
|||
panel.add(headerPanel)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
|
||||
// Add download all button if there are multiple plugins
|
||||
if (matchingPluginStatuses.size > 1) {
|
||||
val downloadAllPanel = JPanel(FlowLayout(FlowLayout.LEFT))
|
||||
downloadAllPanel.background = VIEW_BACKGROUND_COLOR
|
||||
val downloadAllButton = JButton("Download All")
|
||||
downloadAllButton.background = TITLE_BAR_COLOR
|
||||
downloadAllButton.foreground = secondaryColor
|
||||
downloadAllButton.font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
downloadAllButton.addActionListener {
|
||||
startMultiplePluginDownloads(matchingPluginStatuses)
|
||||
}
|
||||
downloadAllPanel.add(downloadAllButton)
|
||||
panel.add(downloadAllPanel)
|
||||
panel.add(Box.createVerticalStrut(5))
|
||||
}
|
||||
|
||||
// Add matching plugin statuses
|
||||
for (pluginStatus in matchingPluginStatuses) {
|
||||
System.out.println("Adding plugin to UI: ${pluginStatus.name}")
|
||||
|
|
@ -352,6 +373,12 @@ object ReflectiveEditorView : View {
|
|||
panel.add(infoPanel, BorderLayout.CENTER)
|
||||
panel.add(controlsPanel, BorderLayout.EAST)
|
||||
|
||||
// Add right-click context menu (except for KondoKit itself)
|
||||
if (packageName != "KondoKit") {
|
||||
val popupMenu = createPluginContextMenu(plugin, pluginInfo, packageName, false)
|
||||
addContextMenuToPanel(panel, popupMenu)
|
||||
}
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
|
|
@ -391,6 +418,12 @@ object ReflectiveEditorView : View {
|
|||
panel.add(infoPanel, BorderLayout.CENTER)
|
||||
panel.add(controlsPanel, BorderLayout.EAST)
|
||||
|
||||
// Add right-click context menu (except for KondoKit itself)
|
||||
if (pluginName != "KondoKit") {
|
||||
val popupMenu = createPluginContextMenu(null, null, pluginName, true)
|
||||
addContextMenuToPanel(panel, popupMenu)
|
||||
}
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
|
|
@ -453,12 +486,22 @@ object ReflectiveEditorView : View {
|
|||
actionButton.foreground = secondaryColor
|
||||
actionButton.font = Font("RuneScape Small", Font.PLAIN, 14)
|
||||
|
||||
if (pluginStatus.isInstalled) {
|
||||
// Progress bar for downloads
|
||||
val progressBar = JProgressBar(0, 100)
|
||||
progressBar.isStringPainted = true
|
||||
progressBar.isVisible = false
|
||||
progressBar.string = "Downloading..."
|
||||
|
||||
if (pluginStatus.isDownloading) {
|
||||
actionButton.isVisible = false
|
||||
progressBar.isVisible = true
|
||||
progressBar.value = pluginStatus.downloadProgress
|
||||
} else if (pluginStatus.isInstalled) {
|
||||
if (pluginStatus.needsUpdate) {
|
||||
actionButton.text = "Update"
|
||||
actionButton.addActionListener {
|
||||
// TODO: Implement update functionality
|
||||
showToast(mainPanel, "Update functionality not yet implemented", JOptionPane.INFORMATION_MESSAGE)
|
||||
// Start downloading the plugin to update it
|
||||
startPluginDownload(pluginStatus)
|
||||
}
|
||||
} else {
|
||||
actionButton.text = "Installed"
|
||||
|
|
@ -467,8 +510,8 @@ object ReflectiveEditorView : View {
|
|||
} else {
|
||||
actionButton.text = "Download"
|
||||
actionButton.addActionListener {
|
||||
// TODO: Implement download functionality
|
||||
showToast(mainPanel, "Download functionality not yet implemented", JOptionPane.INFORMATION_MESSAGE)
|
||||
// Start downloading the plugin
|
||||
startPluginDownload(pluginStatus)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -494,11 +537,18 @@ object ReflectiveEditorView : View {
|
|||
val controlsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 5, 0))
|
||||
controlsPanel.background = WIDGET_COLOR
|
||||
controlsPanel.add(actionButton)
|
||||
controlsPanel.add(progressBar)
|
||||
controlsPanel.add(toggleSwitch)
|
||||
|
||||
panel.add(infoPanel, BorderLayout.CENTER)
|
||||
panel.add(controlsPanel, BorderLayout.EAST)
|
||||
|
||||
// Add right-click context menu (except for KondoKit itself)
|
||||
if (pluginStatus.name != "KondoKit") {
|
||||
val popupMenu = createPluginContextMenu(null, null, pluginStatus.name, false)
|
||||
addContextMenuToPanel(panel, popupMenu)
|
||||
}
|
||||
|
||||
return panel
|
||||
}
|
||||
|
||||
|
|
@ -810,26 +860,51 @@ object ReflectiveEditorView : View {
|
|||
customToolTipWindow!!.isVisible = true
|
||||
}
|
||||
|
||||
// Helper method to reset a ScrollablePanel to the top
|
||||
private fun resetScrollablePanel(scrollablePanel: ScrollablePanel) {
|
||||
// Access the content panel and reset its position
|
||||
// Since we can't directly access the private fields, we'll use reflection
|
||||
// Add this helper function to read plugin properties from a disabled plugin directory
|
||||
private fun readDisabledPluginProperties(pluginName: String): PluginProperties? {
|
||||
try {
|
||||
val contentField = ScrollablePanel::class.java.getDeclaredField("content")
|
||||
contentField.isAccessible = true
|
||||
val contentPanel = contentField.get(scrollablePanel) as JPanel
|
||||
val disabledDir = File(pluginsDirectory, "disabled")
|
||||
val pluginDir = File(disabledDir, pluginName)
|
||||
val propertiesFile = File(pluginDir, "plugin.properties")
|
||||
|
||||
// Reset the location to the top
|
||||
contentPanel.setLocation(0, 0)
|
||||
|
||||
// Force a repaint
|
||||
scrollablePanel.revalidate()
|
||||
scrollablePanel.repaint()
|
||||
if (propertiesFile.exists()) {
|
||||
val content = propertiesFile.readText()
|
||||
return parsePluginProperties(content)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// If reflection fails, at least try to repaint
|
||||
scrollablePanel.revalidate()
|
||||
scrollablePanel.repaint()
|
||||
System.out.println("Error reading properties for disabled plugin $pluginName: ${e.message}")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Helper function to parse plugin.properties content
|
||||
private fun parsePluginProperties(content: String): PluginProperties {
|
||||
var author = "Unknown"
|
||||
var version = "Unknown"
|
||||
var description = "No description available"
|
||||
|
||||
val lines = content.split("\n")
|
||||
for (line in lines) {
|
||||
val parts = line.split("=")
|
||||
if (parts.size == 2) {
|
||||
val key = parts[0].trim()
|
||||
val value = parts[1].replace("\"", "").replace("'", "").trim()
|
||||
|
||||
when {
|
||||
key.startsWith("AUTHOR", ignoreCase = true) -> {
|
||||
author = value
|
||||
}
|
||||
key.startsWith("VERSION", ignoreCase = true) -> {
|
||||
version = value
|
||||
}
|
||||
key.startsWith("DESCRIPTION", ignoreCase = true) -> {
|
||||
description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PluginProperties(author, version, description)
|
||||
}
|
||||
|
||||
// Helper method to find and reset ScrollablePanel components within a container
|
||||
|
|
@ -868,6 +943,148 @@ object ReflectiveEditorView : View {
|
|||
return loadedPluginNames
|
||||
}
|
||||
|
||||
// Method to start downloading a plugin
|
||||
private fun startPluginDownload(pluginStatus: PluginStatus) {
|
||||
val gitLabPlugin = pluginStatus.gitLabPlugin
|
||||
if (gitLabPlugin == null) {
|
||||
showToast(mainPanel, "Plugin information not available", JOptionPane.ERROR_MESSAGE)
|
||||
return
|
||||
}
|
||||
|
||||
// Log the download URL for debugging
|
||||
val downloadUrl = PluginDownloadManager.testDownloadUrl(gitLabPlugin)
|
||||
System.out.println("Download URL for plugin ${gitLabPlugin.path}: $downloadUrl")
|
||||
|
||||
// Update plugin status to show downloading
|
||||
val updatedStatuses = pluginStatuses.map {
|
||||
if (it.name == pluginStatus.name) {
|
||||
it.copy(isDownloading = true, downloadProgress = 0)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
pluginStatuses = updatedStatuses
|
||||
|
||||
// Refresh UI to show progress bar
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
|
||||
// Start the download
|
||||
PluginDownloadManager.downloadPlugin(gitLabPlugin, object : PluginDownloadManager.DownloadProgressCallback {
|
||||
override fun onProgress(pluginName: String, progress: Int) {
|
||||
// Update progress in plugin statuses
|
||||
val updatedStatuses = pluginStatuses.map {
|
||||
if (it.name == pluginName) {
|
||||
it.copy(downloadProgress = progress)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
pluginStatuses = updatedStatuses
|
||||
|
||||
// Refresh UI to show progress
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onComplete(pluginName: String, success: Boolean, errorMessage: String?) {
|
||||
// Update plugin status
|
||||
val updatedStatuses = pluginStatuses.map {
|
||||
if (it.name == pluginName) {
|
||||
it.copy(isDownloading = false, downloadProgress = if (success) 100 else 0)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
pluginStatuses = updatedStatuses
|
||||
|
||||
// Show result to user
|
||||
if (success) {
|
||||
showToast(mainPanel, "Plugin downloaded successfully!", JOptionPane.INFORMATION_MESSAGE)
|
||||
// Reload plugins to make the newly downloaded plugin available
|
||||
PluginRepository.reloadPlugins()
|
||||
} else {
|
||||
showToast(mainPanel, "Failed to download plugin: ${errorMessage ?: "Unknown error"}", JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
|
||||
// Refresh UI
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Method to start downloading multiple plugins
|
||||
private fun startMultiplePluginDownloads(pluginStatuses: List<PluginStatus>) {
|
||||
val gitLabPlugins = pluginStatuses.mapNotNull { it.gitLabPlugin }
|
||||
|
||||
if (gitLabPlugins.isEmpty()) {
|
||||
showToast(mainPanel, "No valid plugins to download", JOptionPane.ERROR_MESSAGE)
|
||||
return
|
||||
}
|
||||
|
||||
// Update all plugin statuses to show downloading
|
||||
val pluginNames = pluginStatuses.map { it.name }.toSet()
|
||||
val updatedStatuses = pluginStatuses.map {
|
||||
if (pluginNames.contains(it.name)) {
|
||||
it.copy(isDownloading = true, downloadProgress = 0)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
this.pluginStatuses = updatedStatuses
|
||||
|
||||
// Refresh UI to show progress bars
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
|
||||
// Counter to track completed downloads
|
||||
val completedCount = AtomicInteger(0)
|
||||
val totalCount = gitLabPlugins.size
|
||||
val failedPlugins = mutableListOf<String>()
|
||||
|
||||
// Start the downloads
|
||||
PluginDownloadManager.downloadPlugins(gitLabPlugins) { pluginName, success, errorMessage ->
|
||||
// Update progress in plugin statuses
|
||||
val updatedStatuses = this.pluginStatuses.map {
|
||||
if (it.name == pluginName) {
|
||||
it.copy(isDownloading = false, downloadProgress = if (success) 100 else 0)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
this.pluginStatuses = updatedStatuses
|
||||
|
||||
// Track completion
|
||||
if (!success) {
|
||||
failedPlugins.add(pluginName)
|
||||
}
|
||||
|
||||
val completed = completedCount.incrementAndGet()
|
||||
|
||||
// If all downloads are complete, show final message
|
||||
if (completed == totalCount) {
|
||||
SwingUtilities.invokeLater {
|
||||
if (failedPlugins.isEmpty()) {
|
||||
showToast(mainPanel, "All plugins downloaded successfully!", JOptionPane.INFORMATION_MESSAGE)
|
||||
// Reload plugins to make the newly downloaded plugins available
|
||||
PluginRepository.reloadPlugins()
|
||||
} else {
|
||||
val failedList = failedPlugins.joinToString(", ")
|
||||
showToast(mainPanel, "Completed with errors. Failed plugins: $failedList", JOptionPane.WARNING_MESSAGE)
|
||||
}
|
||||
|
||||
// Refresh UI
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update plugin statuses by comparing installed and remote plugins
|
||||
private fun updatePluginStatuses() {
|
||||
val loadedPluginNames = getLoadedPluginNames()
|
||||
|
|
@ -875,6 +1092,12 @@ object ReflectiveEditorView : View {
|
|||
|
||||
System.out.println("Updating plugin statuses. Loaded plugins: ${loadedPluginNames.joinToString(", ")}")
|
||||
|
||||
// Get disabled plugin names and their versions
|
||||
val disabledPluginInfo = getDisabledPluginInfo()
|
||||
|
||||
// Handle duplicate plugins (loaded and disabled) - delete the disabled version
|
||||
handleDuplicatePlugins(loadedPluginNames, disabledPluginInfo)
|
||||
|
||||
// Process remote plugins
|
||||
for (gitLabPlugin in gitLabPlugins) {
|
||||
val pluginName = gitLabPlugin.path
|
||||
|
|
@ -883,7 +1106,15 @@ object ReflectiveEditorView : View {
|
|||
val author = gitLabPlugin.pluginProperties?.author ?: "Unknown"
|
||||
|
||||
val isLoaded = loadedPluginNames.contains(pluginName)
|
||||
System.out.println("Processing plugin: $pluginName, isLoaded: $isLoaded")
|
||||
val isDisabled = disabledPluginInfo.containsKey(pluginName)
|
||||
val disabledVersion = disabledPluginInfo[pluginName]
|
||||
|
||||
System.out.println("Processing plugin: $pluginName, isLoaded: $isLoaded, isDisabled: $isDisabled, disabledVersion: $disabledVersion")
|
||||
|
||||
// Check if this plugin is currently being downloaded
|
||||
val existingStatus = pluginStatuses.find { it.name == pluginName }
|
||||
val isDownloading = existingStatus?.isDownloading ?: false
|
||||
val downloadProgress = existingStatus?.downloadProgress ?: 0
|
||||
|
||||
if (isLoaded) {
|
||||
// Plugin is currently loaded
|
||||
|
|
@ -895,10 +1126,31 @@ object ReflectiveEditorView : View {
|
|||
author = author,
|
||||
gitLabPlugin = gitLabPlugin,
|
||||
isInstalled = true,
|
||||
needsUpdate = false // We assume it's up-to-date since it's loaded
|
||||
needsUpdate = false, // We assume it's up-to-date since it's loaded
|
||||
isDownloading = isDownloading,
|
||||
downloadProgress = downloadProgress
|
||||
))
|
||||
} else if (isDisabled) {
|
||||
// Plugin is disabled, check if versions match
|
||||
val versionsMatch = disabledVersion != null && disabledVersion == remoteVersion
|
||||
if (!versionsMatch) {
|
||||
// Versions don't match, show update option
|
||||
statuses.add(PluginStatus(
|
||||
name = pluginName,
|
||||
installedVersion = disabledVersion,
|
||||
remoteVersion = remoteVersion,
|
||||
description = description,
|
||||
author = author,
|
||||
gitLabPlugin = gitLabPlugin,
|
||||
isInstalled = true, // It's installed but disabled
|
||||
needsUpdate = true, // Needs update since versions don't match
|
||||
isDownloading = isDownloading,
|
||||
downloadProgress = downloadProgress
|
||||
))
|
||||
}
|
||||
// If versions match, we don't add it to the list since there's no point showing it
|
||||
} else {
|
||||
// Plugin is not loaded
|
||||
// Plugin is not installed at all
|
||||
statuses.add(PluginStatus(
|
||||
name = pluginName,
|
||||
installedVersion = null,
|
||||
|
|
@ -907,7 +1159,9 @@ object ReflectiveEditorView : View {
|
|||
author = author,
|
||||
gitLabPlugin = gitLabPlugin,
|
||||
isInstalled = false,
|
||||
needsUpdate = false
|
||||
needsUpdate = false,
|
||||
isDownloading = isDownloading,
|
||||
downloadProgress = downloadProgress
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -915,4 +1169,145 @@ object ReflectiveEditorView : View {
|
|||
System.out.println("Updated plugin statuses. Total statuses: ${statuses.size}")
|
||||
pluginStatuses = statuses
|
||||
}
|
||||
|
||||
// Helper method to handle duplicate plugins (loaded and disabled)
|
||||
private fun handleDuplicatePlugins(loadedPluginNames: Set<String>, disabledPluginInfo: Map<String, String>) {
|
||||
for (pluginName in loadedPluginNames) {
|
||||
if (disabledPluginInfo.containsKey(pluginName)) {
|
||||
System.out.println("Found duplicate plugin: $pluginName. Deleting disabled version.")
|
||||
try {
|
||||
val disabledDir = File(pluginsDirectory, "disabled")
|
||||
val pluginDir = File(disabledDir, pluginName)
|
||||
if (pluginDir.exists() && pluginDir.isDirectory) {
|
||||
if (deleteRecursively(pluginDir)) {
|
||||
System.out.println("Successfully deleted disabled version of $pluginName")
|
||||
} else {
|
||||
System.out.println("Failed to delete disabled version of $pluginName")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
System.out.println("Error deleting disabled version of $pluginName: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to get disabled plugin names and their versions
|
||||
private fun getDisabledPluginInfo(): Map<String, String> {
|
||||
val disabledPluginInfo = mutableMapOf<String, String>()
|
||||
val disabledDir = File(pluginsDirectory, "disabled")
|
||||
|
||||
if (disabledDir.exists() && disabledDir.isDirectory) {
|
||||
val disabledPlugins = disabledDir.listFiles { file -> file.isDirectory } ?: arrayOf()
|
||||
|
||||
for (pluginDir in disabledPlugins) {
|
||||
try {
|
||||
val propertiesFile = File(pluginDir, "plugin.properties")
|
||||
if (propertiesFile.exists()) {
|
||||
val content = propertiesFile.readText()
|
||||
val properties = parsePluginProperties(content)
|
||||
disabledPluginInfo[pluginDir.name] = properties.version
|
||||
System.out.println("Found disabled plugin: ${pluginDir.name}, version: ${properties.version}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
System.out.println("Error reading properties for disabled plugin ${pluginDir.name}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return disabledPluginInfo
|
||||
}
|
||||
|
||||
// Helper method to add context menu to a panel
|
||||
private fun addContextMenuToPanel(panel: JPanel, popupMenu: JPopupMenu) {
|
||||
val rightClickListener = object : MouseAdapter() {
|
||||
override fun mousePressed(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) {
|
||||
popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mouseReleased(e: MouseEvent) {
|
||||
if (e.isPopupTrigger) {
|
||||
popupMenu.show(e.component, e.x, e.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
panel.addMouseListener(rightClickListener)
|
||||
}
|
||||
|
||||
// Helper method to create context menu for plugins
|
||||
private fun createPluginContextMenu(plugin: Plugin?, pluginInfo: PluginInfo?, pluginName: String, isDisabled: Boolean): JPopupMenu {
|
||||
val popupMenu = PopupMenuComponent()
|
||||
|
||||
// Add "Delete" menu item
|
||||
popupMenu.addMenuItem("Delete Plugin") {
|
||||
deletePlugin(pluginName, isDisabled)
|
||||
}
|
||||
|
||||
return popupMenu
|
||||
}
|
||||
|
||||
// Helper method to delete a plugin
|
||||
private fun deletePlugin(pluginName: String, isDisabled: Boolean) {
|
||||
try {
|
||||
val pluginDir = if (isDisabled) {
|
||||
File(File(pluginsDirectory, "disabled"), pluginName)
|
||||
} else {
|
||||
File(pluginsDirectory, pluginName)
|
||||
}
|
||||
|
||||
if (pluginDir.exists() && pluginDir.isDirectory) {
|
||||
// Recursively delete the directory
|
||||
if (deleteRecursively(pluginDir)) {
|
||||
showToast(mainPanel, "Plugin deleted successfully", JOptionPane.INFORMATION_MESSAGE)
|
||||
// Refresh the plugin list view
|
||||
SwingUtilities.invokeLater {
|
||||
addPlugins(reflectiveEditorView!!)
|
||||
}
|
||||
} else {
|
||||
showToast(mainPanel, "Failed to delete plugin", JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
} else {
|
||||
showToast(mainPanel, "Plugin directory not found", JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
showToast(mainPanel, "Error deleting plugin: ${e.message}", JOptionPane.ERROR_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to recursively delete a directory
|
||||
private fun deleteRecursively(file: File): Boolean {
|
||||
if (file.isDirectory) {
|
||||
file.listFiles()?.forEach { child ->
|
||||
if (!deleteRecursively(child)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return file.delete()
|
||||
}
|
||||
|
||||
// Helper method to reset a ScrollablePanel to the top
|
||||
private fun resetScrollablePanel(scrollablePanel: ScrollablePanel) {
|
||||
// Access the content panel and reset its position
|
||||
// Since we can't directly access the private fields, we'll use reflection
|
||||
try {
|
||||
val contentField = ScrollablePanel::class.java.getDeclaredField("content")
|
||||
contentField.isAccessible = true
|
||||
val contentPanel = contentField.get(scrollablePanel) as JPanel
|
||||
|
||||
// Reset the location to the top
|
||||
contentPanel.setLocation(0, 0)
|
||||
|
||||
// Force a repaint
|
||||
scrollablePanel.revalidate()
|
||||
scrollablePanel.repaint()
|
||||
} catch (e: Exception) {
|
||||
// If reflection fails, at least try to repaint
|
||||
scrollablePanel.revalidate()
|
||||
scrollablePanel.repaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue